├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── checkstyle-suppressions.xml ├── checkstyle.xml ├── pom.xml └── src ├── main ├── java │ └── su │ │ └── litvak │ │ └── chromecast │ │ └── api │ │ └── v2 │ │ ├── AppEvent.java │ │ ├── Application.java │ │ ├── CastChannel.java │ │ ├── Channel.java │ │ ├── ChromeCast.java │ │ ├── ChromeCastConnectionEvent.java │ │ ├── ChromeCastConnectionEventListener.java │ │ ├── ChromeCastException.java │ │ ├── ChromeCastSpontaneousEvent.java │ │ ├── ChromeCastSpontaneousEventListener.java │ │ ├── ChromeCasts.java │ │ ├── ChromeCastsListener.java │ │ ├── Device.java │ │ ├── EventListenerHolder.java │ │ ├── Item.java │ │ ├── JacksonHelper.java │ │ ├── Media.java │ │ ├── MediaStatus.java │ │ ├── Message.java │ │ ├── MultizoneStatus.java │ │ ├── Namespace.java │ │ ├── RandomString.java │ │ ├── Request.java │ │ ├── Response.java │ │ ├── StandardMessage.java │ │ ├── StandardRequest.java │ │ ├── StandardResponse.java │ │ ├── Status.java │ │ ├── Track.java │ │ ├── Util.java │ │ ├── Volume.java │ │ └── X509TrustAllManager.java └── resources │ └── su │ └── litvak │ └── justdlna │ └── chromecast │ └── v2 │ └── cast_channel.proto └── test ├── java └── su │ └── litvak │ └── chromecast │ └── api │ └── v2 │ ├── ConnectionLostTest.java │ ├── CustomRequestTest.java │ ├── DeviceTest.java │ ├── EventListenerHolderTest.java │ ├── FixtureHelper.java │ ├── InterruptionTest.java │ ├── MediaStatusTest.java │ ├── MediaTest.java │ ├── MockedChromeCast.java │ ├── MultizoneStatusTest.java │ ├── StatusTest.java │ └── UtilTest.java └── resources ├── device-added.json ├── device-removed.json ├── device-updated.json ├── keystore.jks ├── mediaStatus-audio-with-extraStatus.json ├── mediaStatus-chromecast-audio.json ├── mediaStatus-no-metadataType.json ├── mediaStatus-pandora.json ├── mediaStatus-single.json ├── mediaStatus-unknown-metadataType.json ├── mediaStatus-with-idleReason.json ├── mediaStatus-with-videoinfo.json ├── mediaStatus-without-idleReason.json ├── multizoneStatus.json ├── simplelogger.properties ├── status-backdrop-1.18.json ├── status-backdrop-1.19.json ├── status-backdrop-1.28.json ├── status-chrome-mirroring-1.19.json ├── status-chrome-mirroring-1.28.json ├── status-spotify.json └── timetick.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | target 3 | *.iml 4 | .idea 5 | 6 | *.class 7 | 8 | # Mobile Tools for Java (J2ME) 9 | .mtj.tmp/ 10 | 11 | # Package Files # 12 | *.jar 13 | *.war 14 | *.ear 15 | 16 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 17 | hs_err_pid* 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk8 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ChromeCast Java API v2 [![Build Status](https://travis-ci.org/vitalidze/chromecast-java-api-v2.svg?branch=master)](https://travis-ci.org/vitalidze/chromecast-java-api-v2) 2 | ====================== 3 | 4 | At the moment I have started implementing this library, there was a java [implementation of V1 Google ChromeCast protocol](https://github.com/entertailion/Caster), which seems to be deprecated and does not work for newly created applications. The new V2 protocol is implemented by tools that come with Cast SDK, which is available for Android, iOS and Chrome Extension as javascript. Also there is a third party [implementation of V2 in Node.js](https://github.com/vincentbernat/nodecastor). This project is a third party implementation of Google ChromeCast V2 protocol in java. 5 | 6 | Install 7 | ------- 8 | 9 | Library is available in maven central. Put lines below into you project's `pom.xml` file: 10 | 11 | ```xml 12 | 13 | ... 14 | 15 | su.litvak.chromecast 16 | api-v2 17 | 0.11.3 18 | 19 | ... 20 | 21 | ``` 22 | 23 | Or to `build.gradle` (`mavenCentral()` repository should be included in appropriate block): 24 | 25 | ```groovy 26 | dependencies { 27 | // ... 28 | compile 'su.litvak.chromecast:api-v2:0.11.3' 29 | // ... 30 | } 31 | ``` 32 | 33 | Build 34 | ----- 35 | 36 | To build library from sources: 37 | 38 | 1) Clone github repo 39 | 40 | $ git clone https://github.com/vitalidze/chromecast-java-api-v2.git 41 | 42 | 2) Change to the cloned repo folder and run `mvn install` 43 | 44 | $ cd chromecast-java-api-v2 45 | $ mvn install 46 | 47 | 3) Then it could be included into project's `pom.xml` from local repository: 48 | 49 | ```xml 50 | 51 | ... 52 | 53 | su.litvak.chromecast 54 | api-v2 55 | 0.11.4-SNAPSHOT 56 | 57 | ... 58 | 59 | ``` 60 | 61 | Usage 62 | ----- 63 | 64 | This is still a work in progress. The API is not stable, the quality is pretty low and there are a lot of bugs. 65 | 66 | To use the library, you first need to discover what Chromecast devices are available on the network. 67 | 68 | ```java 69 | ChromeCasts.startDiscovery(); 70 | ``` 71 | 72 | Then wait until some device discovered and it will be available in list. Then device should be connected. After that one can invoke several available operations, like check device status, application availability and launch application: 73 | 74 | ```java 75 | ChromeCast chromecast = ChromeCasts.get().get(0); 76 | // Connect (optional) 77 | // Needed only when 'autoReconnect' is 'false'. 78 | // Usually not needed and connection will be established automatically. 79 | // chromecast.connect(); 80 | // Get device status 81 | Status status = chromecast.getStatus(); 82 | // Run application if it's not already running 83 | if (chromecast.isAppAvailable("APP_ID") && !status.isAppRunning("APP_ID")) { 84 | Application app = chromecast.launchApp("APP_ID"); 85 | } 86 | ``` 87 | 88 | To start playing media in currently running media receiver: 89 | 90 | ```java 91 | // play media URL directly 92 | chromecast.load("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"); 93 | // play media URL with additional parameters, such as media title and thumbnail image 94 | chromecast.load("Big Buck Bunny", // Media title 95 | "images/BigBuckBunny.jpg", // URL to thumbnail based on media URL 96 | "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", // media URL 97 | null // media content type (optional, will be discovered automatically) 98 | ); 99 | ``` 100 | 101 | Then playback may be controlled with following methods: 102 | 103 | ```java 104 | // pause playback 105 | chromecast.pause(); 106 | // continue playback 107 | chromecast.play(); 108 | // rewind (move to specified position (in seconds) 109 | chromecast.seek(120); 110 | // update volume 111 | chromecast.setVolume(0.5f); 112 | // mute 113 | chromecast.setMuted(true); 114 | // unmute (will set up volume to value before muting) 115 | chromecast.setMuted(false); 116 | ``` 117 | 118 | Also there are utility methods to get current chromecast status (running app, etc.) and currently played media status: 119 | 120 | ```java 121 | Status status = chromecast.getStatus(); 122 | MediaStatus mediaStatus = chromecast.getMediaStatus(); 123 | ``` 124 | 125 | Current running application may be stopped by calling `stopApp()` method without arguments: 126 | 127 | ```java 128 | // Stop currently running application 129 | chromecast.stopApp(); 130 | ``` 131 | 132 | Don't forget to close connection to ChromeCast device by calling `disconnect()`: 133 | 134 | ```java 135 | // Disconnect from device 136 | chromecast.disconnect(); 137 | ``` 138 | 139 | Finally, stop device discovery: 140 | 141 | ```java 142 | ChromeCasts.stopDiscovery(); 143 | ``` 144 | 145 | Alternatively, ChromeCast device object may be created without discovery if address of chromecast device is known: 146 | 147 | ```java 148 | ChromeCast chromecast = new ChromeCast("192.168.10.36"); 149 | ``` 150 | 151 | Since `v.0.9.0` there is a possibility to send custom requests using `send()` methods. It is required to implement at least `Request` interface for an objects being sent to the running application. If some response is expected then `Response` interface must be implemented. For example to send request to the [DashCast](https://github.com/stestagg/dashcast) application: 152 | 153 | `Request` interface implementation 154 | 155 | ````java 156 | public class DashCastRequest implements Request { 157 | @JsonProperty 158 | final String url; 159 | @JsonProperty 160 | final boolean force; 161 | @JsonProperty 162 | final boolean reload; 163 | @JsonProperty("reload_time") 164 | final int reloadTime; 165 | 166 | private Long requestId; 167 | 168 | public DashCastRequest(String url, 169 | boolean force, 170 | boolean reload, 171 | int reloadTime) { 172 | this.url = url; 173 | this.force = force; 174 | this.reload = reload; 175 | this.reloadTime = reloadTime; 176 | } 177 | 178 | @Override 179 | public Long getRequestId() { 180 | return requestId; 181 | } 182 | 183 | @Override 184 | public void setRequestId(Long requestId) { 185 | this.requestId = requestId; 186 | } 187 | } 188 | ```` 189 | 190 | Sending request 191 | 192 | ````java 193 | chromecast.send("urn:x-cast:es.offd.dashcast", new DashCastRequest("http://yandex.ru", true, false, 0)); 194 | ```` 195 | 196 | This is it for now. It covers all my needs, but if someone is interested in more methods, I am open to make improvements. 197 | 198 | Useful links 199 | ------------ 200 | 201 | * [Implementation of V1 protocol in Node.js](https://github.com/wearefractal/nodecast) 202 | * [Console application implementing V1 protocol in java](https://github.com/entertailion/Caster) 203 | * [GUI application in java using V1 protocol to send media from local machine to ChromeCast](https://github.com/entertailion/Fling) 204 | * [Implementation of V2 protocol in Node.js](https://github.com/vincentbernat/nodecastor) 205 | * [CastV2 protocol description](https://github.com/thibauts/node-castv2#protocol-description) 206 | * [CastV2 media player implementation in Node.js](https://github.com/thibauts/node-castv2-client) 207 | * [Library for Python 2 and 3 to communicate with the Google Chromecast](https://github.com/balloob/pychromecast) 208 | * [CastV2 API protocol POC implementation in Python](https://github.com/minektur/chromecast-python-poc) 209 | * [Most recent .proto file for CastV2 protocol](https://github.com/chromium/chromium/blob/master/components/cast_channel/proto/cast_channel.proto) 210 | 211 | Projects using library 212 | ---------------------- 213 | 214 | * [UniversalMediaServer](https://github.com/UniversalMediaServer/UniversalMediaServer) - powerful server application that serves media to various types of receivers (including ChromeCast) 215 | * [SwingChromecast](https://github.com/DylanMeeus/SwingChromecast) - A graphical user interface to interact with your chromecasts. (Written in Java 8 with swing) 216 | 217 | 218 | License 219 | ------- 220 | 221 | (Apache v2.0 license) 222 | 223 | Copyright (c) 2014 Vitaly Litvak vitavaque@gmail.com 224 | -------------------------------------------------------------------------------- /checkstyle-suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 21 | 22 | 23 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | su.litvak.chromecast 5 | api-v2 6 | 0.11.4-SNAPSHOT 7 | jar 8 | 9 | ChromeCast Java API v2 10 | Java implementation of ChromeCast V2 protocol client 11 | https://github.com/vitalidze/chromecast-java-api-v2 12 | 13 | 14 | 15 | Apache License, Version 2.0 16 | http://www.apache.org/licenses/LICENSE-2.0.txt 17 | repo 18 | 19 | 20 | 21 | 22 | https://github.com/vitalidze/chromecast-java-api-v2 23 | scm:git:https://github.com/vitalidze/chromecast-java-api-v2.git 24 | HEAD 25 | 26 | 27 | 28 | UTF-8 29 | [2.9.1,2.9.99],[2.10.1,2.10.99] 30 | [1.7.2,1.7.100] 31 | 3.1.1 32 | 8.31 33 | 34 | 35 | 36 | 37 | org.jmdns 38 | jmdns 39 | [3.5.1,3.5.5] 40 | 41 | 42 | 43 | com.google.protobuf 44 | protobuf-java 45 | 2.6.0 46 | 47 | 48 | 49 | com.fasterxml.jackson.core 50 | jackson-annotations 51 | ${jackson.version} 52 | 53 | 54 | 55 | com.fasterxml.jackson.core 56 | jackson-databind 57 | ${jackson.version} 58 | 59 | 60 | 61 | org.slf4j 62 | slf4j-api 63 | ${slf4j.version} 64 | 65 | 66 | 67 | junit 68 | junit 69 | 4.13.1 70 | test 71 | 72 | 73 | 74 | org.slf4j 75 | slf4j-simple 76 | ${slf4j.version} 77 | test 78 | 79 | 80 | 81 | 82 | 83 | release-sign-artifacts 84 | 85 | 86 | performRelease 87 | true 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-gpg-plugin 95 | 1.4 96 | 97 | ${gpg.passphrase} 98 | 99 | 100 | 101 | sign-artifacts 102 | verify 103 | 104 | sign 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | org.apache.maven.plugins 119 | maven-checkstyle-plugin 120 | ${checkstyle.plugin.version} 121 | 122 | 123 | com.puppycrawl.tools 124 | checkstyle 125 | ${checkstyle.version} 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | org.apache.felix 134 | maven-bundle-plugin 135 | 3.0.1 136 | 137 | 138 | bundle-manifest 139 | process-classes 140 | 141 | manifest 142 | 143 | 144 | 145 | 146 | 147 | org.apache.maven.plugins 148 | maven-compiler-plugin 149 | 3.2 150 | 151 | 1.6 152 | 1.6 153 | 154 | 155 | 156 | maven-jar-plugin 157 | 2.6 158 | 159 | 160 | ${project.build.outputDirectory}/META-INF/MANIFEST.MF 161 | 162 | 163 | 164 | 165 | org.apache.maven.plugins 166 | maven-release-plugin 167 | 2.5 168 | 169 | v@{project.version} 170 | -Dgpg.passphrase=${gpg.passphrase} 171 | 172 | 173 | 174 | org.apache.maven.plugins 175 | maven-checkstyle-plugin 176 | ${checkstyle.plugin.version} 177 | 178 | 179 | validate 180 | validate 181 | 182 | checkstyle.xml 183 | UTF-8 184 | true 185 | true 186 | true 187 | 188 | 189 | check 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | sonatype-nexus-snapshots 200 | Sonatype Nexus snapshot repository 201 | https://oss.sonatype.org/content/repositories/snapshots 202 | 203 | 204 | sonatype-nexus-staging 205 | Sonatype Nexus release repository 206 | https://oss.sonatype.org/service/local/staging/deploy/maven2 207 | 208 | 209 | 210 | 211 | 212 | 213 | org.apache.maven.plugins 214 | maven-checkstyle-plugin 215 | ${checkstyle.plugin.version} 216 | 217 | checkstyle.xml 218 | UTF-8 219 | true 220 | true 221 | 222 | 223 | 224 | 225 | checkstyle 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | vitaly 236 | Vitaly Litvak 237 | vitavaque@gmail.com 238 | 239 | 240 | 241 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/AppEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | /** 21 | * A custom event sent by a receiver app. 22 | */ 23 | public class AppEvent { 24 | @JsonProperty 25 | public final String namespace; 26 | @JsonProperty 27 | public final String message; 28 | 29 | AppEvent(String namespace, String message) { 30 | this.namespace = namespace; 31 | this.message = message; 32 | } 33 | 34 | @Override 35 | public final String toString() { 36 | return String.format("AppEvent{namespace: %s, message: %s}", this.namespace, this.message); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Application.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | import java.util.Arrays; 21 | import java.util.Collections; 22 | import java.util.List; 23 | 24 | /** 25 | * Application descriptor. 26 | */ 27 | public class Application { 28 | public final String id; 29 | public final String iconUrl; 30 | public final String name; 31 | public final String sessionId; 32 | public final String statusText; 33 | public final String transportId; 34 | public final boolean isIdleScreen; 35 | public final boolean launchedFromCloud; 36 | public final List namespaces; 37 | 38 | public Application(@JsonProperty("appId") String id, 39 | @JsonProperty("iconUrl") String iconUrl, 40 | @JsonProperty("displayName") String name, 41 | @JsonProperty("sessionId") String sessionId, 42 | @JsonProperty("statusText") String statusText, 43 | @JsonProperty("isIdleScreen") boolean isIdleScreen, 44 | @JsonProperty("launchedFromCloud") boolean launchedFromCloud, 45 | @JsonProperty("transportId") String transportId, 46 | @JsonProperty("namespaces") List namespaces) { 47 | this.id = id; 48 | this.iconUrl = iconUrl; 49 | this.name = name; 50 | this.sessionId = sessionId; 51 | this.statusText = statusText; 52 | this.transportId = transportId; 53 | this.namespaces = namespaces == null ? Collections.emptyList() : namespaces; 54 | this.isIdleScreen = isIdleScreen; 55 | this.launchedFromCloud = launchedFromCloud; 56 | } 57 | 58 | @Override 59 | public final String toString() { 60 | final String namespacesString = this.namespaces == null ? "" : Arrays.toString(this.namespaces.toArray()); 61 | 62 | return String.format("Application{id: %s, name: %s, sessionId: %s, statusText: %s, transportId: %s," 63 | + " isIdleScreen: %b, launchedFromCloud: %b, namespaces: %s}", 64 | this.id, this.name, this.sessionId, this.statusText, this.transportId, 65 | this.isIdleScreen, this.launchedFromCloud, namespacesString); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/ChromeCast.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import javax.jmdns.JmDNS; 19 | import javax.jmdns.ServiceInfo; 20 | 21 | import java.io.IOException; 22 | import java.security.GeneralSecurityException; 23 | import java.util.HashMap; 24 | import java.util.Map; 25 | 26 | import static su.litvak.chromecast.api.v2.Util.getContentType; 27 | import static su.litvak.chromecast.api.v2.Util.getMediaTitle; 28 | 29 | /** 30 | * ChromeCast device - main object used for interaction with ChromeCast dongle. 31 | */ 32 | public class ChromeCast { 33 | public static final String SERVICE_TYPE = "_googlecast._tcp.local."; 34 | 35 | private final EventListenerHolder eventListenerHolder = new EventListenerHolder(); 36 | 37 | private String name; 38 | private final String address; 39 | private final int port; 40 | private String appsURL; 41 | private String application; 42 | private Channel channel; 43 | private boolean autoReconnect = true; 44 | 45 | private String title; 46 | private String appTitle; 47 | private String model; 48 | 49 | ChromeCast(JmDNS mDNS, String name) { 50 | this.name = name; 51 | ServiceInfo serviceInfo = mDNS.getServiceInfo(SERVICE_TYPE, name); 52 | this.address = serviceInfo.getInet4Addresses()[0].getHostAddress(); 53 | this.port = serviceInfo.getPort(); 54 | this.appsURL = serviceInfo.getURLs().length == 0 ? null : serviceInfo.getURLs()[0]; 55 | this.application = serviceInfo.getApplication(); 56 | 57 | this.title = serviceInfo.getPropertyString("fn"); 58 | this.appTitle = serviceInfo.getPropertyString("rs"); 59 | this.model = serviceInfo.getPropertyString("md"); 60 | } 61 | 62 | public ChromeCast(String address) { 63 | this(address, 8009); 64 | } 65 | 66 | public ChromeCast(String address, int port) { 67 | this.address = address; 68 | this.port = port; 69 | } 70 | 71 | /** 72 | * @return The technical name of the device. Usually something like Chromecast-e28835678bc02247abcdef112341278f. 73 | */ 74 | public final String getName() { 75 | return name; 76 | } 77 | 78 | public final void setName(String name) { 79 | this.name = name; 80 | } 81 | 82 | /** 83 | * @return The IP address of the device. 84 | */ 85 | public final String getAddress() { 86 | return address; 87 | } 88 | 89 | /** 90 | * @return The TCP port number that the device is listening to. 91 | */ 92 | public final int getPort() { 93 | return port; 94 | } 95 | 96 | public final String getAppsURL() { 97 | return appsURL; 98 | } 99 | 100 | public final void setAppsURL(String appsURL) { 101 | this.appsURL = appsURL; 102 | } 103 | 104 | /** 105 | * @return The mDNS service name. Usually "googlecast". 106 | * 107 | * @see #getRunningApp() 108 | */ 109 | public final String getApplication() { 110 | return application; 111 | } 112 | 113 | public final void setApplication(String application) { 114 | this.application = application; 115 | } 116 | 117 | /** 118 | * @return The name of the device as entered by the person who installed it. 119 | * Usually something like "Living Room Chromecast". 120 | */ 121 | public final String getTitle() { 122 | return title; 123 | } 124 | 125 | /** 126 | * @return The title of the app that is currently running, or empty string in case of the backdrop. 127 | * Usually something like "YouTube" or "Spotify", but could also be, say, the URL of a web page being mirrored. 128 | */ 129 | public final String getAppTitle() { 130 | return appTitle; 131 | } 132 | 133 | /** 134 | * @return The model of the device. Usually "Chromecast" or, if Chromecast is built into your TV, 135 | * the model of your TV. 136 | */ 137 | public final String getModel() { 138 | return model; 139 | } 140 | 141 | /** 142 | * Returns the {@link #channel}. May open it if autoReconnect is set to "true" (default value) 143 | * and it's not yet or no longer open. 144 | * @return an open channel. 145 | */ 146 | private synchronized Channel channel() throws IOException { 147 | if (autoReconnect) { 148 | try { 149 | connect(); 150 | } catch (GeneralSecurityException e) { 151 | throw new IOException(e); 152 | } 153 | } 154 | 155 | return channel; 156 | } 157 | 158 | private String getTransportId(Application runningApp) { 159 | return runningApp.transportId == null ? runningApp.sessionId : runningApp.transportId; 160 | } 161 | 162 | public final synchronized void connect() throws IOException, GeneralSecurityException { 163 | if (channel == null || channel.isClosed()) { 164 | channel = new Channel(this.address, this.port, this.eventListenerHolder); 165 | channel.open(); 166 | } 167 | } 168 | 169 | public final synchronized void disconnect() throws IOException { 170 | if (channel == null) { 171 | return; 172 | } 173 | 174 | channel.close(); 175 | channel = null; 176 | } 177 | 178 | public final boolean isConnected() { 179 | return channel != null && !channel.isClosed(); 180 | } 181 | 182 | /** 183 | * Changes behaviour for opening/closing of connection with ChromeCast device. If set to "true" (default value) 184 | * then connection will be re-established on every request in case it is not present yet, or has been lost. 185 | * "false" value means manual control over connection with ChromeCast device, i.e. calling connect() 186 | * or disconnect() methods when needed. 187 | * 188 | * @param autoReconnect true means controlling connection with ChromeCast device automatically, false - manually 189 | * @see #connect() 190 | * @see #disconnect() 191 | */ 192 | public void setAutoReconnect(boolean autoReconnect) { 193 | this.autoReconnect = autoReconnect; 194 | } 195 | 196 | /** 197 | * @return current value of autoReconnect setting, which controls opening/closing of connection 198 | * with ChromeCast device 199 | * 200 | * @see #setAutoReconnect(boolean) 201 | */ 202 | public boolean isAutoReconnect() { 203 | return autoReconnect; 204 | } 205 | 206 | /** 207 | * Set up how much time to wait until request is processed (in milliseconds). 208 | * @param requestTimeout value in milliseconds until request times out waiting for response 209 | */ 210 | public void setRequestTimeout(long requestTimeout) { 211 | channel.setRequestTimeout(requestTimeout); 212 | } 213 | 214 | /** 215 | * @return current chromecast status - volume, running applications, etc. 216 | * @throws IOException 217 | */ 218 | public final Status getStatus() throws IOException { 219 | return channel().getStatus(); 220 | } 221 | 222 | /** 223 | * @return descriptor of currently running application 224 | * @throws IOException 225 | */ 226 | public final Application getRunningApp() throws IOException { 227 | Status status = getStatus(); 228 | return status.getRunningApp(); 229 | } 230 | 231 | /** 232 | * @param appId application identifier 233 | * @return true if application is available to this chromecast device, false otherwise 234 | * @throws IOException 235 | */ 236 | public final boolean isAppAvailable(String appId) throws IOException { 237 | return channel().isAppAvailable(appId); 238 | } 239 | 240 | /** 241 | * @param appId application identifier 242 | * @return true if application with specified identifier is running now 243 | * @throws IOException 244 | */ 245 | public final boolean isAppRunning(String appId) throws IOException { 246 | Status status = getStatus(); 247 | return status.getRunningApp() != null && appId.equals(status.getRunningApp().id); 248 | } 249 | 250 | /** 251 | * @param appId application identifier 252 | * @return application descriptor if app successfully launched, null otherwise 253 | * @throws IOException 254 | */ 255 | public final Application launchApp(String appId) throws IOException { 256 | Status status = channel().launch(appId); 257 | return status == null ? null : status.getRunningApp(); 258 | } 259 | 260 | /** 261 | *

Stops currently running application

262 | * 263 | *

If no application is running at the moment then exception is thrown.

264 | * 265 | * @throws IOException 266 | */ 267 | public final void stopApp() throws IOException { 268 | Application runningApp = getRunningApp(); 269 | if (runningApp == null) { 270 | throw new ChromeCastException("No application is running in ChromeCast"); 271 | } 272 | channel().stop(runningApp.sessionId); 273 | } 274 | 275 | /** 276 | *

Stops the session with the given identifier.

277 | * 278 | * @param sessionId session identifier 279 | * @throws IOException 280 | */ 281 | public final void stopSession(String sessionId) throws IOException { 282 | channel().stop(sessionId); 283 | } 284 | 285 | /** 286 | * @param level volume level from 0 to 1 to set 287 | */ 288 | public final void setVolume(float level) throws IOException { 289 | channel().setVolume(new Volume(level, false, Volume.DEFAULT_INCREMENT, 290 | Volume.DEFAULT_INCREMENT.doubleValue(), Volume.DEFAULT_CONTROL_TYPE)); 291 | } 292 | 293 | /** 294 | * ChromeCast does not allow you to jump levels too quickly to avoid blowing speakers. 295 | * Setting by increment allows us to easily get the level we want 296 | * 297 | * @param level volume level from 0 to 1 to set 298 | * @throws IOException 299 | * @see sender 300 | */ 301 | public final void setVolumeByIncrement(float level) throws IOException { 302 | Volume volume = this.getStatus().volume; 303 | float total = volume.level; 304 | 305 | if (volume.increment <= 0f) { 306 | throw new ChromeCastException("Volume.increment is <= 0"); 307 | } 308 | 309 | // With floating points we always have minor decimal variations, using the Math.min/max 310 | // works around this issue 311 | // Increase volume 312 | if (level > total) { 313 | while (total < level) { 314 | total = Math.min(total + volume.increment, level); 315 | setVolume(total); 316 | } 317 | // Decrease Volume 318 | } else if (level < total) { 319 | while (total > level) { 320 | total = Math.max(total - volume.increment, level); 321 | setVolume(total); 322 | } 323 | } 324 | } 325 | 326 | /** 327 | * @param muted is to mute or not 328 | */ 329 | public final void setMuted(boolean muted) throws IOException { 330 | channel().setVolume(new Volume(null, muted, Volume.DEFAULT_INCREMENT, 331 | Volume.DEFAULT_INCREMENT.doubleValue(), Volume.DEFAULT_CONTROL_TYPE)); 332 | } 333 | 334 | /** 335 | *

If no application is running at the moment then exception is thrown.

336 | * 337 | * @return current media status, state, time, playback rate, etc. 338 | * @throws IOException 339 | */ 340 | public final MediaStatus getMediaStatus() throws IOException { 341 | Application runningApp = getRunningApp(); 342 | if (runningApp == null) { 343 | throw new ChromeCastException("No application is running in ChromeCast"); 344 | } 345 | return channel().getMediaStatus(getTransportId(runningApp)); 346 | } 347 | 348 | /** 349 | *

Resume paused media playback

350 | * 351 | *

If no application is running at the moment then exception is thrown.

352 | * 353 | * @throws IOException 354 | */ 355 | public final void play() throws IOException { 356 | Status status = getStatus(); 357 | Application runningApp = status.getRunningApp(); 358 | if (runningApp == null) { 359 | throw new ChromeCastException("No application is running in ChromeCast"); 360 | } 361 | MediaStatus mediaStatus = channel().getMediaStatus(getTransportId(runningApp)); 362 | if (mediaStatus == null) { 363 | throw new ChromeCastException("ChromeCast has invalid state to resume media playback"); 364 | } 365 | channel().play(getTransportId(runningApp), runningApp.sessionId, mediaStatus.mediaSessionId); 366 | } 367 | 368 | /** 369 | *

Pause current playback

370 | * 371 | *

If no application is running at the moment then exception is thrown.

372 | * 373 | * @throws IOException 374 | */ 375 | public final void pause() throws IOException { 376 | Status status = getStatus(); 377 | Application runningApp = status.getRunningApp(); 378 | if (runningApp == null) { 379 | throw new ChromeCastException("No application is running in ChromeCast"); 380 | } 381 | MediaStatus mediaStatus = channel().getMediaStatus(getTransportId(runningApp)); 382 | if (mediaStatus == null) { 383 | throw new ChromeCastException("ChromeCast has invalid state to pause media playback"); 384 | } 385 | channel().pause(getTransportId(runningApp), runningApp.sessionId, mediaStatus.mediaSessionId); 386 | } 387 | 388 | /** 389 | *

Moves current playback time point to specified value

390 | * 391 | *

If no application is running at the moment then exception is thrown.

392 | * 393 | * @param time time point between zero and media duration 394 | * @throws IOException 395 | */ 396 | public final void seek(double time) throws IOException { 397 | Status status = getStatus(); 398 | Application runningApp = status.getRunningApp(); 399 | if (runningApp == null) { 400 | throw new ChromeCastException("No application is running in ChromeCast"); 401 | } 402 | MediaStatus mediaStatus = channel().getMediaStatus(getTransportId(runningApp)); 403 | if (mediaStatus == null) { 404 | throw new ChromeCastException("ChromeCast has invalid state to seek media playback"); 405 | } 406 | channel().seek(getTransportId(runningApp), runningApp.sessionId, mediaStatus.mediaSessionId, time); 407 | } 408 | 409 | /** 410 | *

Loads and starts playing media in specified URL

411 | * 412 | *

If no application is running at the moment then exception is thrown.

413 | * 414 | * @param url media url 415 | * @return The new media status that resulted from loading the media. 416 | * @throws IOException 417 | */ 418 | public final MediaStatus load(String url) throws IOException { 419 | return load(getMediaTitle(url), null, url, getContentType(url)); 420 | } 421 | 422 | /** 423 | *

Loads and starts playing specified media

424 | * 425 | *

If no application is running at the moment then exception is thrown.

426 | * 427 | * @param mediaTitle name to be displayed 428 | * @param thumb url of video thumbnail to be displayed, relative to media url 429 | * @param url media url 430 | * @param contentType MIME content type 431 | * @return The new media status that resulted from loading the media. 432 | * @throws IOException 433 | */ 434 | public final MediaStatus load(String mediaTitle, String thumb, String url, String contentType) throws IOException { 435 | Status status = getStatus(); 436 | Application runningApp = status.getRunningApp(); 437 | if (runningApp == null) { 438 | throw new ChromeCastException("No application is running in ChromeCast"); 439 | } 440 | Map metadata = new HashMap(2); 441 | metadata.put("title", mediaTitle); 442 | metadata.put("thumb", thumb); 443 | return channel().load(getTransportId(runningApp), runningApp.sessionId, new Media(url, 444 | contentType == null ? getContentType(url) : contentType, null, null, null, 445 | metadata, null, null), true, 0d, null); 446 | } 447 | 448 | /** 449 | *

Loads and starts playing specified media

450 | * 451 | *

If no application is running at the moment then exception is thrown.

452 | * 453 | * @param media The media to load and play. 454 | * See 455 | * https://developers.google.com/cast/docs/reference/messages#Load for more details. 456 | * @return The new media status that resulted from loading the media. 457 | * @throws IOException 458 | */ 459 | public final MediaStatus load(final Media media) throws IOException { 460 | Status status = getStatus(); 461 | Application runningApp = status.getRunningApp(); 462 | if (runningApp == null) { 463 | throw new ChromeCastException("No application is running in ChromeCast"); 464 | } 465 | Media mediaToPlay; 466 | if (media.contentType == null) { 467 | mediaToPlay = new Media(media.url, getContentType(media.url), media.duration, media.streamType, 468 | media.customData, media.metadata, media.textTrackStyle, media.tracks); 469 | } else { 470 | mediaToPlay = media; 471 | } 472 | return channel().load(getTransportId(runningApp), runningApp.sessionId, mediaToPlay, true, 0d, null); 473 | } 474 | 475 | /** 476 | *

Sends some generic request to the currently running application.

477 | * 478 | *

If no application is running at the moment then exception is thrown.

479 | * 480 | * @param namespace request namespace 481 | * @param request request object 482 | * @param responseClass class of the response for proper deserialization 483 | * @param type of response 484 | * @return deserialized response 485 | * @throws IOException 486 | */ 487 | public final T send(String namespace, Request request, Class responseClass) 488 | throws IOException { 489 | Status status = getStatus(); 490 | Application runningApp = status.getRunningApp(); 491 | if (runningApp == null) { 492 | throw new ChromeCastException("No application is running in ChromeCast"); 493 | } 494 | return channel().sendGenericRequest(getTransportId(runningApp), namespace, request, responseClass); 495 | } 496 | 497 | /** 498 | *

Sends some generic request to the currently running application. 499 | * No response is expected as a result of this call.

500 | * 501 | *

If no application is running at the moment then exception is thrown.

502 | * 503 | * @param namespace request namespace 504 | * @param request request object 505 | * @throws IOException 506 | */ 507 | public final void send(String namespace, Request request) throws IOException { 508 | send(namespace, request, null); 509 | } 510 | 511 | public final void registerListener(ChromeCastSpontaneousEventListener listener) { 512 | this.eventListenerHolder.registerListener(listener); 513 | } 514 | 515 | public final void unregisterListener(ChromeCastSpontaneousEventListener listener) { 516 | this.eventListenerHolder.unregisterListener(listener); 517 | } 518 | 519 | public final void registerConnectionListener(ChromeCastConnectionEventListener listener) { 520 | this.eventListenerHolder.registerConnectionListener(listener); 521 | } 522 | 523 | public final void unregisterConnectionListener(ChromeCastConnectionEventListener listener) { 524 | this.eventListenerHolder.unregisterConnectionListener(listener); 525 | } 526 | 527 | @Override 528 | public final String toString() { 529 | return String.format("ChromeCast{name: %s, title: %s, model: %s, address: %s, port: %d}", 530 | this.name, this.title, this.model, this.address, this.port); 531 | } 532 | } 533 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/ChromeCastConnectionEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | /** 19 | * Event fired when connection to ChromeCast device is either established or closed. 20 | */ 21 | public class ChromeCastConnectionEvent { 22 | /** 23 | * Identifies type of event. 24 | * 25 | * true value means connection was established. 26 | * false value means connection was closed. 27 | */ 28 | private final boolean connected; 29 | 30 | ChromeCastConnectionEvent(final boolean connected) { 31 | this.connected = connected; 32 | } 33 | 34 | public final boolean isConnected() { 35 | return connected; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/ChromeCastConnectionEventListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | /** 19 | * The listener interface for receiving connection open/close events. The class that is interested in processing 20 | * connection events implements this interface, and object create with that class is registered 21 | * with ChromeCast instance using the registerConnectionListener method. 22 | * When connection event occurs, that object's connectionEventReceived is invoked. 23 | * 24 | * @see ChromeCastConnectionEvent 25 | */ 26 | public interface ChromeCastConnectionEventListener { 27 | 28 | void connectionEventReceived(ChromeCastConnectionEvent event); 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/ChromeCastException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import java.io.IOException; 19 | 20 | /** 21 | * Generic error, which may happen during interaction with ChromeCast device. Contains some descriptive message. 22 | */ 23 | public class ChromeCastException extends IOException { 24 | public ChromeCastException(String message) { 25 | super(message); 26 | } 27 | 28 | public ChromeCastException(String message, Throwable cause) { 29 | super(message, cause); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/ChromeCastSpontaneousEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.databind.JsonNode; 19 | 20 | /** 21 | *

Identifies that a broadcast message was received from "receiver application". This message was not triggered 22 | * by a sender request.

23 | * 24 | * @see 25 | * https://developers.google.com/cast/docs/reference/messages#MediaMess 26 | */ 27 | public class ChromeCastSpontaneousEvent { 28 | 29 | /** 30 | * Type of a spontaneous events. Some events are expected and can contain some useful known data. For the rest 31 | * there is UNKNOWN type of spontaneous event with generic data. 32 | */ 33 | public enum SpontaneousEventType { 34 | 35 | /** 36 | * Data type will be {@link MediaStatus}. 37 | */ 38 | MEDIA_STATUS(MediaStatus.class), 39 | 40 | /** 41 | * Data type will be {@link Status}. 42 | */ 43 | STATUS(Status.class), 44 | 45 | /** 46 | * Data type will be {@link AppEvent}. 47 | */ 48 | APPEVENT(AppEvent.class), 49 | 50 | /** 51 | * Special event usually received when session is stopped. 52 | */ 53 | CLOSE(Object.class), 54 | 55 | /** 56 | * Data type will be {@link com.fasterxml.jackson.databind.JsonNode}. 57 | */ 58 | UNKNOWN(JsonNode.class); 59 | 60 | private final Class dataClass; 61 | 62 | SpontaneousEventType(Class dataClass) { 63 | this.dataClass = dataClass; 64 | } 65 | 66 | public Class getDataClass() { 67 | return this.dataClass; 68 | } 69 | } 70 | 71 | private final SpontaneousEventType type; 72 | private final Object data; 73 | 74 | public ChromeCastSpontaneousEvent(final SpontaneousEventType type, final Object data) { 75 | if (!type.getDataClass().isAssignableFrom(data.getClass())) { 76 | throw new IllegalArgumentException("Data type " + data.getClass() + " does not match type for event " 77 | + type.getDataClass()); 78 | } 79 | this.type = type; 80 | this.data = data; 81 | } 82 | 83 | public final SpontaneousEventType getType() { 84 | return this.type; 85 | } 86 | 87 | public final Object getData() { 88 | return this.data; 89 | } 90 | 91 | public final T getData(Class cls) { 92 | if (!cls.isAssignableFrom(this.type.getDataClass())) { 93 | throw new IllegalArgumentException("Requested type " + cls + " does not match type for event " 94 | + this.type.getDataClass()); 95 | } 96 | return cls.cast(this.data); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/ChromeCastSpontaneousEventListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | /** 19 | * The listener interface for receiving spontaneous events. The class that is interested in processing spontaneous 20 | * events implements this interface, and object create with that class is registered with ChromeCast 21 | * instance using the registerListener method. When spontaneous event occurs, that object's 22 | * spontaneousEventReceived is invoked. 23 | * 24 | * @see ChromeCastSpontaneousEvent 25 | * @see 26 | * https://developers.google.com/cast/docs/reference/messages#MediaMess 27 | */ 28 | public interface ChromeCastSpontaneousEventListener { 29 | 30 | void spontaneousEventReceived(ChromeCastSpontaneousEvent event); 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/ChromeCasts.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import javax.jmdns.JmDNS; 19 | import javax.jmdns.ServiceEvent; 20 | import javax.jmdns.ServiceListener; 21 | 22 | import java.io.IOException; 23 | import java.net.InetAddress; 24 | import java.util.ArrayList; 25 | import java.util.Collections; 26 | import java.util.List; 27 | 28 | /** 29 | * Utility class that discovers ChromeCast devices and holds references to all of them. 30 | */ 31 | public final class ChromeCasts { 32 | private static final ChromeCasts INSTANCE = new ChromeCasts(); 33 | private final MyServiceListener listener = new MyServiceListener(); 34 | 35 | private JmDNS mDNS; 36 | 37 | private final List listeners = new ArrayList(); 38 | private final List chromeCasts = Collections.synchronizedList(new ArrayList()); 39 | 40 | private ChromeCasts() { 41 | } 42 | 43 | /** Returns a copy of the currently seen chrome casts. 44 | * @return a copy of the currently seen chromecast devices. 45 | */ 46 | public static List get() { 47 | return new ArrayList(INSTANCE.chromeCasts); 48 | } 49 | 50 | /** Hidden service listener to receive callbacks. 51 | * Is hidden to avoid messing with it. 52 | */ 53 | private class MyServiceListener implements ServiceListener { 54 | @Override 55 | public void serviceAdded(ServiceEvent se) { 56 | if (se.getInfo() != null) { 57 | ChromeCast device = new ChromeCast(mDNS, se.getInfo().getName()); 58 | chromeCasts.add(device); 59 | for (ChromeCastsListener nextListener : listeners) { 60 | nextListener.newChromeCastDiscovered(device); 61 | } 62 | } 63 | } 64 | 65 | @Override 66 | public void serviceRemoved(ServiceEvent se) { 67 | if (ChromeCast.SERVICE_TYPE.equals(se.getType())) { 68 | // We have a ChromeCast device unregistering 69 | List copy = get(); 70 | ChromeCast deviceRemoved = null; 71 | // Probably better keep a map to better lookup devices 72 | for (ChromeCast device : copy) { 73 | if (device.getName().equals(se.getInfo().getName())) { 74 | deviceRemoved = device; 75 | chromeCasts.remove(device); 76 | break; 77 | } 78 | } 79 | if (deviceRemoved != null) { 80 | for (ChromeCastsListener nextListener : listeners) { 81 | nextListener.chromeCastRemoved(deviceRemoved); 82 | } 83 | } 84 | } 85 | } 86 | 87 | @Override 88 | public void serviceResolved(ServiceEvent se) { 89 | // intentionally blank 90 | } 91 | } 92 | 93 | private void doStartDiscovery(InetAddress addr) throws IOException { 94 | if (mDNS == null) { 95 | chromeCasts.clear(); 96 | 97 | if (addr != null) { 98 | mDNS = JmDNS.create(addr); 99 | } else { 100 | mDNS = JmDNS.create(); 101 | } 102 | mDNS.addServiceListener(ChromeCast.SERVICE_TYPE, listener); 103 | } 104 | } 105 | 106 | private void doStopDiscovery() throws IOException { 107 | if (mDNS != null) { 108 | mDNS.close(); 109 | mDNS = null; 110 | } 111 | } 112 | 113 | /** 114 | * Starts ChromeCast device discovery. 115 | */ 116 | public static void startDiscovery() throws IOException { 117 | INSTANCE.doStartDiscovery(null); 118 | } 119 | 120 | /** 121 | * Starts ChromeCast device discovery. 122 | * 123 | * @param addr the address of the interface that should be used for discovery 124 | */ 125 | public static void startDiscovery(InetAddress addr) throws IOException { 126 | INSTANCE.doStartDiscovery(addr); 127 | } 128 | 129 | /** 130 | * Stops ChromeCast device discovery. 131 | */ 132 | public static void stopDiscovery() throws IOException { 133 | INSTANCE.doStopDiscovery(); 134 | } 135 | 136 | /** 137 | * Restarts discovery by sequentially calling 'stop' and 'start' methods. 138 | */ 139 | public static void restartDiscovery() throws IOException { 140 | stopDiscovery(); 141 | startDiscovery(); 142 | } 143 | 144 | /** 145 | * Restarts discovery by sequentially calling 'stop' and 'start' methods. 146 | * 147 | * @param addr the address of the interface that should be used for discovery 148 | */ 149 | public static void restartDiscovery(InetAddress addr) throws IOException { 150 | stopDiscovery(); 151 | startDiscovery(addr); 152 | } 153 | 154 | public static void registerListener(ChromeCastsListener listener) { 155 | if (listener != null) { 156 | INSTANCE.listeners.add(listener); 157 | } 158 | } 159 | 160 | public static void unregisterListener(ChromeCastsListener listener) { 161 | if (listener != null) { 162 | INSTANCE.listeners.remove(listener); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/ChromeCastsListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | /** 19 | * The listener interface for discovering ChromeCast devices. 20 | */ 21 | public interface ChromeCastsListener { 22 | 23 | void newChromeCastDiscovered(ChromeCast chromeCast); 24 | 25 | void chromeCastRemoved(ChromeCast chromeCast); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Device.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | /** 21 | * Device descriptor. 22 | */ 23 | public class Device { 24 | public final String name; 25 | public final int capabilities; 26 | public final String deviceId; 27 | public final Volume volume; 28 | 29 | public Device(@JsonProperty("name") String name, 30 | @JsonProperty("capabilities") int capabilities, 31 | @JsonProperty("deviceId") String deviceId, 32 | @JsonProperty("volume") Volume volume) { 33 | this.name = name; 34 | this.capabilities = capabilities; 35 | this.deviceId = deviceId; 36 | this.volume = volume; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/EventListenerHolder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.databind.JsonMappingException; 19 | import com.fasterxml.jackson.databind.JsonNode; 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEvent.SpontaneousEventType; 22 | 23 | import java.io.IOException; 24 | import java.util.Set; 25 | import java.util.concurrent.CopyOnWriteArraySet; 26 | 27 | /** 28 | * Helper class for delivering spontaneous events to their listeners. 29 | */ 30 | class EventListenerHolder implements ChromeCastSpontaneousEventListener, ChromeCastConnectionEventListener { 31 | 32 | private final ObjectMapper jsonMapper = JacksonHelper.createJSONMapper(); 33 | private final Set eventListeners = 34 | new CopyOnWriteArraySet(); 35 | private final Set eventListenersConnection = 36 | new CopyOnWriteArraySet(); 37 | 38 | EventListenerHolder() {} 39 | 40 | public void registerListener(ChromeCastSpontaneousEventListener listener) { 41 | if (listener != null) { 42 | this.eventListeners.add(listener); 43 | } 44 | } 45 | 46 | public void unregisterListener(ChromeCastSpontaneousEventListener listener) { 47 | if (listener != null) { 48 | this.eventListeners.remove(listener); 49 | } 50 | } 51 | 52 | public void deliverEvent(JsonNode json) throws IOException { 53 | if (json == null || this.eventListeners.isEmpty()) { 54 | return; 55 | } 56 | 57 | StandardResponse resp; 58 | if (json.has("responseType")) { 59 | try { 60 | resp = this.jsonMapper.treeToValue(json, StandardResponse.class); 61 | } catch (JsonMappingException jme) { 62 | resp = null; 63 | } 64 | } else { 65 | resp = null; 66 | } 67 | 68 | /* 69 | * The documentation only mentions MEDIA_STATUS as being a possible spontaneous event. 70 | * Though RECEIVER_STATUS has also been observed. 71 | * If others are observed, they should be added here. 72 | * see: https://developers.google.com/cast/docs/reference/messages#MediaMess 73 | */ 74 | if (resp instanceof StandardResponse.MediaStatus) { 75 | StandardResponse.MediaStatus mediaStatusResponse = (StandardResponse.MediaStatus) resp; 76 | // it may be a single media status event 77 | if (mediaStatusResponse.statuses == null) { 78 | if (json.has("media")) { 79 | try { 80 | MediaStatus ms = jsonMapper.treeToValue(json, MediaStatus.class); 81 | spontaneousEventReceived(new ChromeCastSpontaneousEvent(SpontaneousEventType.MEDIA_STATUS, ms)); 82 | } catch (JsonMappingException jme) { 83 | // ignored 84 | } 85 | } 86 | } else { 87 | for (final MediaStatus ms : mediaStatusResponse.statuses) { 88 | spontaneousEventReceived(new ChromeCastSpontaneousEvent(SpontaneousEventType.MEDIA_STATUS, ms)); 89 | } 90 | } 91 | } else if (resp instanceof StandardResponse.Status) { 92 | spontaneousEventReceived(new ChromeCastSpontaneousEvent(SpontaneousEventType.STATUS, 93 | ((StandardResponse.Status) resp).status)); 94 | } else if (resp instanceof StandardResponse.Close) { 95 | spontaneousEventReceived(new ChromeCastSpontaneousEvent(SpontaneousEventType.CLOSE, new Object())); 96 | } else { 97 | spontaneousEventReceived(new ChromeCastSpontaneousEvent(SpontaneousEventType.UNKNOWN, json)); 98 | } 99 | } 100 | 101 | public void deliverAppEvent(AppEvent event) throws IOException { 102 | spontaneousEventReceived(new ChromeCastSpontaneousEvent(SpontaneousEventType.APPEVENT, event)); 103 | } 104 | 105 | @Override 106 | public void spontaneousEventReceived(ChromeCastSpontaneousEvent event) { 107 | for (ChromeCastSpontaneousEventListener listener : this.eventListeners) { 108 | listener.spontaneousEventReceived(event); 109 | } 110 | } 111 | 112 | public void registerConnectionListener(ChromeCastConnectionEventListener listener) { 113 | if (listener != null) { 114 | this.eventListenersConnection.add(listener); 115 | } 116 | } 117 | 118 | public void unregisterConnectionListener(ChromeCastConnectionEventListener listener) { 119 | if (listener != null) { 120 | this.eventListenersConnection.remove(listener); 121 | } 122 | } 123 | 124 | public void deliverConnectionEvent(boolean connected) { 125 | connectionEventReceived(new ChromeCastConnectionEvent(connected)); 126 | } 127 | 128 | @Override 129 | public void connectionEventReceived(ChromeCastConnectionEvent event) { 130 | for (ChromeCastConnectionEventListener listener : this.eventListenersConnection) { 131 | listener.connectionEventReceived(event); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Item.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | import java.util.Arrays; 21 | import java.util.Collections; 22 | import java.util.Map; 23 | 24 | /** 25 | * Media item. 26 | */ 27 | public class Item { 28 | 29 | public final boolean autoplay; 30 | public final Map customData; 31 | public final Media media; 32 | public final long id; 33 | 34 | public Item(@JsonProperty("autoplay") boolean autoplay, 35 | @JsonProperty("customData") Map customData, 36 | @JsonProperty("itemId") long id, 37 | @JsonProperty("media") Media media) { 38 | this.autoplay = autoplay; 39 | this.customData = customData != null ? Collections.unmodifiableMap(customData) : null; 40 | this.id = id; 41 | this.media = media; 42 | } 43 | 44 | @Override 45 | public final int hashCode() { 46 | return Arrays.hashCode(new Object[] {this.autoplay, this.customData, this.id, this.media}); 47 | } 48 | 49 | @Override 50 | public final boolean equals(Object obj) { 51 | if (obj == null) { 52 | return false; 53 | } 54 | if (obj == this) { 55 | return true; 56 | } 57 | if (!(obj instanceof Item)) { 58 | return false; 59 | } 60 | final Item that = (Item) obj; 61 | return this.autoplay == that.autoplay 62 | && this.customData == null ? that.customData == null : this.customData.equals(that.customData) 63 | && this.id == that.id 64 | && this.media == null ? that.media == null : this.media.equals(that.media); 65 | } 66 | 67 | @Override 68 | public final String toString() { 69 | return String.format("Item{id: %s, media: %s}", this.id, this.media); 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/JacksonHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.databind.DeserializationFeature; 19 | import com.fasterxml.jackson.databind.ObjectMapper; 20 | 21 | /** 22 | * Utility class for creating pre-configured instances of JSON mapper. 23 | */ 24 | final class JacksonHelper { 25 | private JacksonHelper() {} 26 | 27 | static ObjectMapper createJSONMapper() { 28 | ObjectMapper jsonMapper = new ObjectMapper(); 29 | jsonMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 30 | return jsonMapper; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Media.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonIgnore; 19 | import com.fasterxml.jackson.annotation.JsonInclude; 20 | import com.fasterxml.jackson.annotation.JsonProperty; 21 | 22 | import java.util.Arrays; 23 | import java.util.Collections; 24 | import java.util.List; 25 | import java.util.Map; 26 | 27 | import static su.litvak.chromecast.api.v2.Media.MetadataType.GENERIC; 28 | 29 | /** 30 | * Media streamed on ChromeCast device. 31 | * 32 | * @see 33 | * https://developers.google.com/cast/docs/reference/receiver/cast.receiver.media.MediaInformation 34 | */ 35 | public class Media { 36 | public static final String METADATA_TYPE = "metadataType"; 37 | public static final String METADATA_ALBUM_ARTIST = "albumArtist"; 38 | public static final String METADATA_ALBUM_NAME = "albumName"; 39 | public static final String METADATA_ARTIST = "artist"; 40 | public static final String METADATA_BROADCAST_DATE = "broadcastDate"; 41 | public static final String METADATA_COMPOSER = "composer"; 42 | public static final String METADATA_CREATION_DATE = "creationDate"; 43 | public static final String METADATA_DISC_NUMBER = "discNumber"; 44 | public static final String METADATA_EPISODE_NUMBER = "episodeNumber"; 45 | public static final String METADATA_HEIGHT = "height"; 46 | public static final String METADATA_IMAGES = "images"; 47 | public static final String METADATA_LOCATION_NAME = "locationName"; 48 | public static final String METADATA_LOCATION_LATITUDE = "locationLatitude"; 49 | public static final String METADATA_LOCATION_LONGITUDE = "locationLongitude"; 50 | public static final String METADATA_RELEASE_DATE = "releaseDate"; 51 | public static final String METADATA_SEASON_NUMBER = "seasonNumber"; 52 | public static final String METADATA_SERIES_TITLE = "seriesTitle"; 53 | public static final String METADATA_STUDIO = "studio"; 54 | public static final String METADATA_SUBTITLE = "subtitle"; 55 | public static final String METADATA_TITLE = "title"; 56 | public static final String METADATA_TRACK_NUMBER = "trackNumber"; 57 | public static final String METADATA_WIDTH = "width"; 58 | 59 | /** 60 | * Type of the data found inside {@link #metadata}. You can access the type with the key {@link #METADATA_TYPE}. 61 | * 62 | * You can access known metadata types using the constants in {@link Media}, such as {@link #METADATA_ALBUM_NAME}. 63 | * 64 | * @see 65 | * https://developers.google.com/cast/docs/reference/ios/interface_g_c_k_media_metadata 66 | * @see 67 | * https://developers.google.com/android/reference/com/google/android/gms/cast/MediaMetadata 68 | */ 69 | public enum MetadataType { 70 | GENERIC, 71 | MOVIE, 72 | TV_SHOW, 73 | MUSIC_TRACK, 74 | PHOTO 75 | } 76 | 77 | /** 78 | *

Stream type.

79 | * 80 | *

Some receivers use upper-case (like Pandora), some use lower-case (like Google Audio), 81 | * duplicate elements to support both.

82 | * 83 | * @see 84 | * https://developers.google.com/cast/docs/reference/receiver/cast.receiver.media#.StreamType 85 | */ 86 | public enum StreamType { 87 | BUFFERED, buffered, 88 | LIVE, live, 89 | NONE, none 90 | } 91 | 92 | @JsonProperty 93 | @JsonInclude(JsonInclude.Include.NON_NULL) 94 | public final Map metadata; 95 | 96 | @JsonProperty("contentId") 97 | public final String url; 98 | 99 | @JsonProperty 100 | @JsonInclude(JsonInclude.Include.NON_NULL) 101 | public final Double duration; 102 | 103 | @JsonProperty 104 | @JsonInclude(JsonInclude.Include.NON_NULL) 105 | public final StreamType streamType; 106 | 107 | @JsonProperty 108 | public final String contentType; 109 | 110 | @JsonProperty 111 | @JsonInclude(JsonInclude.Include.NON_NULL) 112 | public final Map customData; 113 | 114 | @JsonIgnore 115 | public final Map textTrackStyle; 116 | 117 | @JsonIgnore 118 | public final List tracks; 119 | 120 | public Media(String url, String contentType) { 121 | this(url, contentType, null, null); 122 | } 123 | 124 | public Media(String url, String contentType, Double duration, StreamType streamType) { 125 | this(url, contentType, duration, streamType, null, null, null, null); 126 | } 127 | 128 | public Media(@JsonProperty("contentId") String url, 129 | @JsonProperty("contentType") String contentType, 130 | @JsonProperty("duration") Double duration, 131 | @JsonProperty("streamType") StreamType streamType, 132 | @JsonProperty("customData") Map customData, 133 | @JsonProperty("metadata") Map metadata, 134 | @JsonProperty("textTrackStyle") Map textTrackStyle, 135 | @JsonProperty("tracks") List tracks) { 136 | this.url = url; 137 | this.contentType = contentType; 138 | this.duration = duration; 139 | this.streamType = streamType; 140 | this.customData = customData == null ? null : Collections.unmodifiableMap(customData); 141 | this.metadata = metadata == null ? null : Collections.unmodifiableMap(metadata); 142 | this.textTrackStyle = textTrackStyle == null ? null : Collections.unmodifiableMap(textTrackStyle); 143 | this.tracks = tracks == null ? null : Collections.unmodifiableList(tracks); 144 | } 145 | 146 | /** 147 | * @return the type defined by the key {@link #METADATA_TYPE}. 148 | */ 149 | @JsonIgnore 150 | public final MetadataType getMetadataType() { 151 | if (metadata == null || !metadata.containsKey(METADATA_TYPE)) { 152 | return GENERIC; 153 | } 154 | 155 | Integer ordinal = (Integer) metadata.get(METADATA_TYPE); 156 | return ordinal < MetadataType.values().length ? MetadataType.values()[ordinal] : GENERIC; 157 | } 158 | 159 | @Override 160 | public final int hashCode() { 161 | return Arrays.hashCode(new Object[] {this.url, this.contentType, this.streamType, this.duration}); 162 | } 163 | 164 | @Override 165 | public final boolean equals(Object obj) { 166 | if (obj == null) { 167 | return false; 168 | } 169 | if (obj == this) { 170 | return true; 171 | } 172 | if (!(obj instanceof Media)) { 173 | return false; 174 | } 175 | final Media that = (Media) obj; 176 | return this.url == null ? that.url == null : this.url.equals(that.url) 177 | && this.contentType == null ? that.contentType == null : this.contentType.equals(that.contentType) 178 | && this.streamType == null ? that.streamType == null : this.streamType.equals(that.streamType) 179 | && this.duration == null ? that.duration == null : this.duration.equals(that.duration); 180 | } 181 | 182 | @Override 183 | public final String toString() { 184 | return String.format("Media{url: %s, contentType: %s, duration: %s}", 185 | this.url, this.contentType, this.duration); 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/MediaStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | import java.util.Arrays; 21 | import java.util.Collections; 22 | import java.util.List; 23 | import java.util.Map; 24 | 25 | /** 26 | * Current media player status - which media is played, volume, time position, etc. 27 | * 28 | * @see 29 | * https://developers.google.com/cast/docs/reference/receiver/cast.receiver.media.MediaStatus 30 | */ 31 | public class MediaStatus { 32 | /** 33 | * Playback status. 34 | * 35 | * @see 36 | * https://developers.google.com/cast/docs/reference/receiver/cast.receiver.media#.PlayerState 37 | * @see 39 | * https://developers.google.com/cast/docs/reference/receiver/cast.receiver.media#.ExtendedPlayerState 40 | */ 41 | public enum PlayerState { IDLE, BUFFERING, PLAYING, PAUSED, LOADING } 42 | 43 | /** 44 | * @see 45 | * https://developers.google.com/cast/docs/reference/receiver/cast.receiver.media#.repeatMode 46 | */ 47 | public enum RepeatMode { REPEAT_OFF, REPEAT_ALL, REPEAT_SINGLE, REPEAT_ALL_AND_SHUFFLE } 48 | 49 | /** 50 | *

The reason for the player to be in IDLE state.

51 | * 52 | *

Pandora is known to use 'COMPLETED' when the app timesout

53 | * 54 | * @see 55 | * https://developers.google.com/cast/docs/reference/receiver/cast.receiver.media#.IdleReason 56 | */ 57 | public enum IdleReason { CANCELLED, INTERRUPTED, FINISHED, ERROR, COMPLETED } 58 | 59 | public final List activeTrackIds; 60 | public final long mediaSessionId; 61 | public final int playbackRate; 62 | public final PlayerState playerState; 63 | public final Integer currentItemId; 64 | public final double currentTime; 65 | public final Map customData; 66 | public final Integer loadingItemId; 67 | public final List items; 68 | public final Integer preloadedItemId; 69 | public final int supportedMediaCommands; 70 | public final Volume volume; 71 | public final Media media; 72 | public final RepeatMode repeatMode; 73 | public final IdleReason idleReason; 74 | 75 | MediaStatus(@JsonProperty("activeTrackIds") List activeTrackIds, 76 | @JsonProperty("mediaSessionId") long mediaSessionId, 77 | @JsonProperty("playbackRate") int playbackRate, 78 | @JsonProperty("playerState") PlayerState playerState, 79 | @JsonProperty("currentItemId") Integer currentItemId, 80 | @JsonProperty("currentTime") double currentTime, 81 | @JsonProperty("customData") Map customData, 82 | @JsonProperty("loadingItemId") Integer loadingItemId, 83 | @JsonProperty("items") List items, 84 | @JsonProperty("preloadedItemId") Integer preloadedItemId, 85 | @JsonProperty("supportedMediaCommands") int supportedMediaCommands, 86 | @JsonProperty("volume") Volume volume, 87 | @JsonProperty("media") Media media, 88 | @JsonProperty("repeatMode") RepeatMode repeatMode, 89 | @JsonProperty("idleReason") IdleReason idleReason) { 90 | this.activeTrackIds = activeTrackIds != null ? Collections.unmodifiableList(activeTrackIds) : null; 91 | this.mediaSessionId = mediaSessionId; 92 | this.playbackRate = playbackRate; 93 | this.playerState = playerState; 94 | this.currentItemId = currentItemId; 95 | this.currentTime = currentTime; 96 | this.customData = customData != null ? Collections.unmodifiableMap(customData) : null; 97 | this.loadingItemId = loadingItemId; 98 | this.items = items != null ? Collections.unmodifiableList(items) : null; 99 | this.preloadedItemId = preloadedItemId; 100 | this.supportedMediaCommands = supportedMediaCommands; 101 | this.volume = volume; 102 | this.media = media; 103 | this.repeatMode = repeatMode; 104 | this.idleReason = idleReason; 105 | } 106 | 107 | @Override 108 | public final String toString() { 109 | String activeTrackIdsString = this.activeTrackIds == null 110 | ? "" 111 | : Arrays.toString(this.activeTrackIds.toArray()); 112 | String itemsString = this.items == null 113 | ? "" 114 | : Arrays.toString(this.items.toArray()); 115 | String customDataString = this.customData == null 116 | ? "" 117 | : Arrays.toString(this.customData.keySet().toArray()); 118 | 119 | return String.format("MediaStatus{activeTrackIds: %s, mediaSessionId: %d, playbackRate: %d, playerState: %s," 120 | + " currentItemId: %s, currentTime: %f, customData: %s, loadingItemId: %s, items: %s," 121 | + " preloadedItemId: %s, supportedMediaCommands: %d, volume: %s, media: %s, repeatMode: %s," 122 | + " idleReason: %s}", 123 | activeTrackIdsString, this.mediaSessionId, this.playbackRate, this.playerState, this.currentItemId, 124 | this.currentTime, customDataString, this.loadingItemId, itemsString, this.preloadedItemId, 125 | this.supportedMediaCommands, this.volume, this.media, this.repeatMode, this.idleReason); 126 | } 127 | 128 | } 129 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Message.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | /** 19 | * Generic message used in exchange with the ChromeCast device. 20 | */ 21 | public interface Message { 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/MultizoneStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | /** 21 | * Status received in case there a multiple ChomeCast devices in several "zones" (multi-zone setup). 22 | */ 23 | public class MultizoneStatus { 24 | public final Device[] devices; 25 | public final boolean isMultichannel; 26 | 27 | public MultizoneStatus(@JsonProperty("devices") Device[] devices, 28 | @JsonProperty("isMultichannel") boolean isMultichannel) { 29 | this.devices = devices; 30 | this.isMultichannel = isMultichannel; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Namespace.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | /** 21 | * Namespace of the ChromeCast application. 22 | */ 23 | public class Namespace { 24 | public final String name; 25 | 26 | public Namespace(@JsonProperty("name") String name) { 27 | this.name = name; 28 | } 29 | 30 | @Override 31 | public final String toString() { 32 | return String.format("Namespace{%s}", this.name); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/RandomString.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import java.util.Random; 19 | 20 | /** 21 | * Utility class for generating random strings of human-readable characters. 22 | */ 23 | class RandomString { 24 | private static char[] symbols; 25 | 26 | static { 27 | StringBuilder tmp = new StringBuilder(); 28 | for (char ch = '0'; ch <= '9'; ++ch) { 29 | tmp.append(ch); 30 | } 31 | for (char ch = 'a'; ch <= 'z'; ++ch) { 32 | tmp.append(ch); 33 | } 34 | symbols = tmp.toString().toCharArray(); 35 | } 36 | 37 | private final Random random = new Random(); 38 | 39 | private final char[] buf; 40 | 41 | RandomString(int length) { 42 | if (length < 1) { 43 | throw new IllegalArgumentException("length < 1: " + length); 44 | } 45 | buf = new char[length]; 46 | } 47 | 48 | final String nextString() { 49 | for (int idx = 0; idx < buf.length; ++idx) { 50 | buf[idx] = symbols[random.nextInt(symbols.length)]; 51 | } 52 | return new String(buf); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Request.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | /** 21 | * Implementation must be a request object, which can be serialized to JSON with Jackson library. 22 | */ 23 | public interface Request extends Message { 24 | @JsonProperty 25 | Long getRequestId(); 26 | @JsonProperty 27 | void setRequestId(Long requestId); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Response.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | /** 21 | * Implementation must be a response object, which can be serialized to JSON with Jackson library. 22 | */ 23 | public interface Response { 24 | @JsonProperty 25 | Long getRequestId(); 26 | @JsonProperty 27 | void setRequestId(Long requestId); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/StandardMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | import com.fasterxml.jackson.annotation.JsonSubTypes; 20 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 21 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 22 | 23 | /** 24 | * Parent class for transport objects used to communicate with ChromeCast. 25 | */ 26 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type") 27 | @JsonSubTypes({@JsonSubTypes.Type(name = "PING", value = StandardMessage.Ping.class), 28 | @JsonSubTypes.Type(name = "PONG", value = StandardMessage.Pong.class), 29 | @JsonSubTypes.Type(name = "CONNECT", value = StandardMessage.Connect.class), 30 | @JsonSubTypes.Type(name = "GET_STATUS", value = StandardRequest.Status.class), 31 | @JsonSubTypes.Type(name = "GET_APP_AVAILABILITY", value = StandardRequest.AppAvailability.class), 32 | @JsonSubTypes.Type(name = "LAUNCH", value = StandardRequest.Launch.class), 33 | @JsonSubTypes.Type(name = "STOP", value = StandardRequest.Stop.class), 34 | @JsonSubTypes.Type(name = "LOAD", value = StandardRequest.Load.class), 35 | @JsonSubTypes.Type(name = "PLAY", value = StandardRequest.Play.class), 36 | @JsonSubTypes.Type(name = "PAUSE", value = StandardRequest.Pause.class), 37 | @JsonSubTypes.Type(name = "SET_VOLUME", value = StandardRequest.SetVolume.class), 38 | @JsonSubTypes.Type(name = "SEEK", value = StandardRequest.Seek.class)}) 39 | abstract class StandardMessage implements Message { 40 | /** 41 | * Simple "Ping" message to request a reply with "Pong" message. 42 | */ 43 | static class Ping extends StandardMessage {} 44 | 45 | /** 46 | * Simple "Pong" message to reply to "Ping" message. 47 | */ 48 | static class Pong extends StandardMessage {} 49 | 50 | /** 51 | * Some "Origin" required to be sent with the "Connect" request. 52 | */ 53 | @JsonSerialize 54 | static class Origin {} 55 | 56 | /** 57 | * Used to initiate connection with the ChromeCast device. 58 | */ 59 | static class Connect extends StandardMessage { 60 | @JsonProperty 61 | final Origin origin = new Origin(); 62 | } 63 | 64 | public static Ping ping() { 65 | return new Ping(); 66 | } 67 | 68 | public static Pong pong() { 69 | return new Pong(); 70 | } 71 | 72 | public static Connect connect() { 73 | return new Connect(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/StandardRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | import java.util.Map; 21 | 22 | /** 23 | * Parent class for transport object representing messages sent TO ChromeCast device. 24 | */ 25 | abstract class StandardRequest extends StandardMessage implements Request { 26 | Long requestId; 27 | 28 | @Override 29 | public final void setRequestId(Long requestId) { 30 | this.requestId = requestId; 31 | } 32 | 33 | @Override 34 | public final Long getRequestId() { 35 | return requestId; 36 | } 37 | 38 | /** 39 | * Request for current status of ChromeCast device. 40 | */ 41 | static class Status extends StandardRequest {} 42 | 43 | /** 44 | * Request for availability of applications with specified identifiers. 45 | */ 46 | static class AppAvailability extends StandardRequest { 47 | @JsonProperty 48 | final String[] appId; 49 | 50 | AppAvailability(String... appId) { 51 | this.appId = appId; 52 | } 53 | } 54 | 55 | /** 56 | * Request to launch application with specified identifiers. 57 | */ 58 | static class Launch extends StandardRequest { 59 | @JsonProperty 60 | final String appId; 61 | 62 | Launch(@JsonProperty("appId") String appId) { 63 | this.appId = appId; 64 | } 65 | } 66 | 67 | /** 68 | * Request to stop currently running application. 69 | */ 70 | static class Stop extends StandardRequest { 71 | @JsonProperty 72 | final String sessionId; 73 | 74 | Stop(String sessionId) { 75 | this.sessionId = sessionId; 76 | } 77 | } 78 | 79 | /** 80 | * Request to load media. 81 | */ 82 | static class Load extends StandardRequest { 83 | @JsonProperty 84 | final String sessionId; 85 | @JsonProperty 86 | final Media media; 87 | @JsonProperty 88 | final boolean autoplay; 89 | @JsonProperty 90 | final double currentTime; 91 | @JsonProperty 92 | final Object customData; 93 | 94 | Load(String sessionId, Media media, boolean autoplay, double currentTime, 95 | final Map customData) { 96 | this.sessionId = sessionId; 97 | this.media = media; 98 | this.autoplay = autoplay; 99 | this.currentTime = currentTime; 100 | 101 | this.customData = customData == null ? null : new Object() { 102 | @JsonProperty 103 | Map payload = customData; 104 | }; 105 | } 106 | } 107 | 108 | /** 109 | * Abstract request for an action with currently played media. 110 | */ 111 | abstract static class MediaRequest extends StandardRequest { 112 | @JsonProperty 113 | final long mediaSessionId; 114 | @JsonProperty 115 | final String sessionId; 116 | 117 | MediaRequest(long mediaSessionId, String sessionId) { 118 | this.mediaSessionId = mediaSessionId; 119 | this.sessionId = sessionId; 120 | } 121 | } 122 | 123 | /** 124 | * Request to start/resume playback. 125 | */ 126 | static class Play extends MediaRequest { 127 | Play(long mediaSessionId, String sessionId) { 128 | super(mediaSessionId, sessionId); 129 | } 130 | } 131 | 132 | /** 133 | * Request to pause playback. 134 | */ 135 | static class Pause extends MediaRequest { 136 | Pause(long mediaSessionId, String sessionId) { 137 | super(mediaSessionId, sessionId); 138 | } 139 | } 140 | 141 | /** 142 | * Request to change current playback position. 143 | */ 144 | static class Seek extends MediaRequest { 145 | @JsonProperty 146 | final double currentTime; 147 | 148 | Seek(long mediaSessionId, String sessionId, double currentTime) { 149 | super(mediaSessionId, sessionId); 150 | this.currentTime = currentTime; 151 | } 152 | } 153 | 154 | /** 155 | * Request to change volume. 156 | */ 157 | static class SetVolume extends StandardRequest { 158 | @JsonProperty 159 | final Volume volume; 160 | 161 | SetVolume(Volume volume) { 162 | this.volume = volume; 163 | } 164 | } 165 | 166 | static Status status() { 167 | return new Status(); 168 | } 169 | 170 | static AppAvailability appAvailability(String... appId) { 171 | return new AppAvailability(appId); 172 | } 173 | 174 | static Launch launch(String appId) { 175 | return new Launch(appId); 176 | } 177 | 178 | static Stop stop(String sessionId) { 179 | return new Stop(sessionId); 180 | } 181 | 182 | static Load load(String sessionId, Media media, boolean autoplay, double currentTime, 183 | Map customData) { 184 | return new Load(sessionId, media, autoplay, currentTime, customData); 185 | } 186 | 187 | static Play play(String sessionId, long mediaSessionId) { 188 | return new Play(mediaSessionId, sessionId); 189 | } 190 | 191 | static Pause pause(String sessionId, long mediaSessionId) { 192 | return new Pause(mediaSessionId, sessionId); 193 | } 194 | 195 | static Seek seek(String sessionId, long mediaSessionId, double currentTime) { 196 | return new Seek(mediaSessionId, sessionId, currentTime); 197 | } 198 | 199 | static SetVolume setVolume(Volume volume) { 200 | return new SetVolume(volume); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/StandardResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | import com.fasterxml.jackson.annotation.JsonSubTypes; 20 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 21 | 22 | import java.util.Map; 23 | 24 | /** 25 | * Parent class for transport object representing messages received FROM ChromeCast device. 26 | */ 27 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "responseType") 28 | @JsonSubTypes({@JsonSubTypes.Type(name = "PING", value = StandardResponse.Ping.class), 29 | @JsonSubTypes.Type(name = "PONG", value = StandardResponse.Pong.class), 30 | @JsonSubTypes.Type(name = "RECEIVER_STATUS", value = StandardResponse.Status.class), 31 | @JsonSubTypes.Type(name = "GET_APP_AVAILABILITY", value = StandardResponse.AppAvailability.class), 32 | @JsonSubTypes.Type(name = "INVALID_REQUEST", value = StandardResponse.Invalid.class), 33 | @JsonSubTypes.Type(name = "MEDIA_STATUS", value = StandardResponse.MediaStatus.class), 34 | @JsonSubTypes.Type(name = "MULTIZONE_STATUS", value = StandardResponse.MultizoneStatus.class), 35 | @JsonSubTypes.Type(name = "CLOSE", value = StandardResponse.Close.class), 36 | @JsonSubTypes.Type(name = "LOAD_FAILED", value = StandardResponse.LoadFailed.class), 37 | @JsonSubTypes.Type(name = "LAUNCH_ERROR", value = StandardResponse.LaunchError.class), 38 | @JsonSubTypes.Type(name = "DEVICE_ADDED", value = StandardResponse.DeviceAdded.class), 39 | @JsonSubTypes.Type(name = "DEVICE_UPDATED", value = StandardResponse.DeviceUpdated.class), 40 | @JsonSubTypes.Type(name = "DEVICE_REMOVED", value = StandardResponse.DeviceRemoved.class)}) 41 | abstract class StandardResponse implements Response { 42 | Long requestId; 43 | 44 | @Override 45 | public final Long getRequestId() { 46 | return requestId; 47 | } 48 | 49 | @Override 50 | public final void setRequestId(Long requestId) { 51 | this.requestId = requestId; 52 | } 53 | 54 | /** 55 | * Request to send 'Pong' message in reply. 56 | */ 57 | static class Ping extends StandardResponse {} 58 | 59 | /** 60 | * Response in reply to 'Ping' message. 61 | */ 62 | static class Pong extends StandardResponse {} 63 | 64 | /** 65 | * Request to 'Close' connection. 66 | */ 67 | static class Close extends StandardResponse {} 68 | 69 | /** 70 | * Identifies that loading of media has failed. 71 | */ 72 | static class LoadFailed extends StandardResponse {} 73 | 74 | /** 75 | * Request was invalid for some reason. 76 | */ 77 | static class Invalid extends StandardResponse { 78 | final String reason; 79 | 80 | Invalid(@JsonProperty("reason") String reason) { 81 | this.reason = reason; 82 | } 83 | } 84 | 85 | /** 86 | * Application cannot be launched for some reason. 87 | */ 88 | static class LaunchError extends StandardResponse { 89 | final String reason; 90 | 91 | LaunchError(@JsonProperty("reason") String reason) { 92 | this.reason = reason; 93 | } 94 | } 95 | 96 | /** 97 | * Response to "Status" request. 98 | */ 99 | static class Status extends StandardResponse { 100 | @JsonProperty 101 | final su.litvak.chromecast.api.v2.Status status; 102 | 103 | Status(@JsonProperty("status") su.litvak.chromecast.api.v2.Status status) { 104 | this.status = status; 105 | } 106 | } 107 | 108 | /** 109 | * Response to "MediaStatus" request. 110 | */ 111 | static class MediaStatus extends StandardResponse { 112 | final su.litvak.chromecast.api.v2.MediaStatus[] statuses; 113 | 114 | MediaStatus(@JsonProperty("status") su.litvak.chromecast.api.v2.MediaStatus... statuses) { 115 | this.statuses = statuses; 116 | } 117 | } 118 | 119 | /** 120 | * Response to "AppAvailability" request. 121 | */ 122 | static class AppAvailability extends StandardResponse { 123 | @JsonProperty 124 | Map availability; 125 | } 126 | 127 | /** 128 | * Multi-Zone status for the case when there are multiple ChromeCast devices in different zones (rooms). 129 | */ 130 | static class MultizoneStatus extends StandardResponse { 131 | @JsonProperty 132 | final su.litvak.chromecast.api.v2.MultizoneStatus status; 133 | 134 | MultizoneStatus(@JsonProperty("status") su.litvak.chromecast.api.v2.MultizoneStatus status) { 135 | this.status = status; 136 | } 137 | } 138 | 139 | /** 140 | * Received when power is cycled on ChromeCast Audio device in a group. 141 | */ 142 | static class DeviceAdded extends StandardResponse { 143 | final Device device; 144 | 145 | DeviceAdded(@JsonProperty("device") Device device) { 146 | this.device = device; 147 | } 148 | } 149 | 150 | /** 151 | * Received when volume is changed in ChromeCast Audio group. 152 | */ 153 | static class DeviceUpdated extends StandardResponse { 154 | final Device device; 155 | 156 | DeviceUpdated(@JsonProperty("device") Device device) { 157 | this.device = device; 158 | } 159 | } 160 | 161 | /** 162 | * Received when power is cycled on ChromeCast Audio device in a group. 163 | */ 164 | static class DeviceRemoved extends StandardResponse { 165 | final String deviceId; 166 | 167 | DeviceRemoved(@JsonProperty("deviceId") String deviceId) { 168 | this.deviceId = deviceId; 169 | } 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Status.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonIgnore; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | 21 | import java.util.Arrays; 22 | import java.util.Collections; 23 | import java.util.List; 24 | 25 | /** 26 | * Current ChromeCast device status. 27 | */ 28 | public class Status { 29 | public final Volume volume; 30 | public final List applications; 31 | public final boolean activeInput; 32 | public final boolean standBy; 33 | 34 | Status(@JsonProperty("volume") Volume volume, 35 | @JsonProperty("applications") List applications, 36 | @JsonProperty("isActiveInput") boolean activeInput, 37 | @JsonProperty("isStandBy") boolean standBy) { 38 | this.volume = volume; 39 | this.applications = applications == null ? Collections.emptyList() : applications; 40 | this.activeInput = activeInput; 41 | this.standBy = standBy; 42 | } 43 | 44 | @JsonIgnore 45 | public final Application getRunningApp() { 46 | return applications.isEmpty() ? null : applications.get(0); 47 | } 48 | 49 | public final boolean isAppRunning(String appId) { 50 | return getRunningApp() != null && getRunningApp().id.equals(appId); 51 | } 52 | 53 | @Override 54 | public final String toString() { 55 | final String applicationsString = this.applications == null 56 | ? "" 57 | : Arrays.toString(this.applications.toArray()); 58 | 59 | return String.format("Media{volume: %s, applications: %s, activeInput: %b, standBy; %b}", 60 | this.volume, applicationsString, this.activeInput, this.standBy); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Track.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | 20 | /** 21 | * Track meta data information. 22 | * 23 | * @see 24 | * https://developers.google.com/cast/docs/reference/receiver/cast.receiver.media.Track 25 | */ 26 | public class Track { 27 | /** 28 | * Media track type. 29 | * 30 | * @see 31 | * https://developers.google.com/cast/docs/reference/receiver/cast.receiver.media#.TrackType 32 | */ 33 | public enum TrackType { TEXT, AUDIO, VIDEO } 34 | 35 | public final long id; 36 | public final TrackType type; 37 | 38 | public Track(@JsonProperty("trackId") long id, 39 | @JsonProperty("trackType") TrackType type) { 40 | this.id = id; 41 | this.type = type; 42 | } 43 | 44 | @Override 45 | public final String toString() { 46 | return String.format("Track{id: %d, type: %s}", this.id, this.type); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Util.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import java.io.IOException; 19 | import java.net.HttpURLConnection; 20 | import java.net.MalformedURLException; 21 | import java.net.URL; 22 | 23 | /** 24 | * Contains utility methods. 25 | */ 26 | final class Util { 27 | private Util() { 28 | } 29 | 30 | /** 31 | * Converts specified byte array in Big Endian to int. 32 | */ 33 | static int fromArray(byte[] payload) { 34 | return payload[0] << 24 | (payload[1] & 0xFF) << 16 | (payload[2] & 0xFF) << 8 | (payload[3] & 0xFF); 35 | } 36 | 37 | /** 38 | * Converts specified int to byte array in Big Endian. 39 | */ 40 | static byte[] toArray(int value) { 41 | return new byte[] { 42 | (byte) (value >> 24), 43 | (byte) (value >> 16), 44 | (byte) (value >> 8), 45 | (byte) value }; 46 | } 47 | 48 | static String getContentType(String url) { 49 | HttpURLConnection connection = null; 50 | try { 51 | connection = (HttpURLConnection) new URL(url).openConnection(); 52 | connection.connect(); 53 | return connection.getContentType(); 54 | } catch (IOException e) { 55 | } finally { 56 | if (connection != null) { 57 | connection.disconnect(); 58 | } 59 | } 60 | return null; 61 | } 62 | 63 | static String getMediaTitle(String url) { 64 | try { 65 | URL urlObj = new URL(url); 66 | String mediaTitle; 67 | String path = urlObj.getPath(); 68 | int lastIndexOfSlash = path.lastIndexOf('/'); 69 | if (lastIndexOfSlash >= 0 && lastIndexOfSlash + 1 < url.length()) { 70 | mediaTitle = path.substring(lastIndexOfSlash + 1); 71 | int lastIndexOfDot = mediaTitle.lastIndexOf('.'); 72 | if (lastIndexOfDot > 0) { 73 | mediaTitle = mediaTitle.substring(0, lastIndexOfDot); 74 | } 75 | } else { 76 | mediaTitle = path; 77 | } 78 | return mediaTitle.isEmpty() ? url : mediaTitle; 79 | } catch (MalformedURLException mfu) { 80 | return url; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/Volume.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonInclude; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | 21 | import java.util.Arrays; 22 | 23 | /** 24 | * Volume settings. 25 | */ 26 | public class Volume { 27 | static final Float DEFAULT_INCREMENT = 0.05f; 28 | static final String DEFAULT_CONTROL_TYPE = "attenuation"; 29 | @JsonProperty 30 | @JsonInclude(JsonInclude.Include.NON_NULL) 31 | public final Float level; 32 | @JsonProperty 33 | public final boolean muted; 34 | 35 | @JsonProperty 36 | public final Float increment; 37 | @JsonProperty 38 | public final Double stepInterval; 39 | @JsonProperty 40 | public final String controlType; 41 | 42 | public Volume() { 43 | level = -1f; 44 | muted = false; 45 | increment = DEFAULT_INCREMENT; 46 | stepInterval = DEFAULT_INCREMENT.doubleValue(); 47 | controlType = DEFAULT_CONTROL_TYPE; 48 | } 49 | 50 | public Volume(@JsonProperty("level") Float level, 51 | @JsonProperty("muted") boolean muted, 52 | @JsonProperty("increment") Float increment, 53 | @JsonProperty("stepInterval") Double stepInterval, 54 | @JsonProperty("controlType") String controlType 55 | ) { 56 | this.level = level; 57 | this.muted = muted; 58 | if (increment != null && increment > 0f) { 59 | this.increment = increment; 60 | } else { 61 | this.increment = DEFAULT_INCREMENT; 62 | } 63 | if (stepInterval != null && stepInterval > 0d) { 64 | this.stepInterval = stepInterval; 65 | } else { 66 | this.stepInterval = DEFAULT_INCREMENT.doubleValue(); 67 | } 68 | this.controlType = controlType; 69 | } 70 | 71 | @Override 72 | public final int hashCode() { 73 | return Arrays.hashCode(new Object[]{this.level, this.muted, this.increment, 74 | this.stepInterval, this.controlType}); 75 | } 76 | 77 | @Override 78 | public final boolean equals(final Object obj) { 79 | if (obj == null) { 80 | return false; 81 | } 82 | if (obj == this) { 83 | return true; 84 | } 85 | if (!(obj instanceof Volume)) { 86 | return false; 87 | } 88 | final Volume that = (Volume) obj; 89 | return this.level == null ? that.level == null : this.level.equals(that.level) 90 | && this.muted == that.muted 91 | && this.increment == null ? that.increment == null : this.increment.equals(that.increment) 92 | && this.stepInterval == null ? that.stepInterval == null : this.stepInterval.equals(that.stepInterval) 93 | && this.controlType == null ? that.controlType == null : this.controlType.equals(that.controlType); 94 | } 95 | 96 | @Override 97 | public final String toString() { 98 | return String.format("Volume{level: %s, muted: %b, increment: %s, stepInterval: %s, controlType: %s}", 99 | this.level, this.muted, this.increment, this.stepInterval, this.controlType); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/su/litvak/chromecast/api/v2/X509TrustAllManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import javax.net.ssl.X509TrustManager; 19 | 20 | import java.security.cert.X509Certificate; 21 | 22 | /** 23 | * Google Cast's certificate cannot be validated against standard keystore, so use a dummy trust-all manager. 24 | */ 25 | class X509TrustAllManager implements X509TrustManager { 26 | @Override 27 | public X509Certificate[] getAcceptedIssuers() { 28 | return null; 29 | } 30 | @Override 31 | public void checkClientTrusted(X509Certificate[] certs, String authType) {} 32 | @Override 33 | public void checkServerTrusted(X509Certificate[] certs, String authType) {} 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/su/litvak/justdlna/chromecast/v2/cast_channel.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Chromium Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style license that can be 3 | // found in the LICENSE file. 4 | 5 | syntax = "proto2"; 6 | 7 | option optimize_for = LITE_RUNTIME; 8 | 9 | package su.litvak.chromecast.api.v2; 10 | 11 | message CastMessage { 12 | // Always pass a version of the protocol for future compatibility 13 | // requirements. 14 | enum ProtocolVersion { 15 | CASTV2_1_0 = 0; 16 | } 17 | required ProtocolVersion protocol_version = 1; 18 | 19 | // source and destination ids identify the origin and destination of the 20 | // message. They are used to route messages between endpoints that share a 21 | // device-to-device channel. 22 | // 23 | // For messages between applications: 24 | // - The sender application id is a unique identifier generated on behalf of 25 | // the sender application. 26 | // - The receiver id is always the the session id for the application. 27 | // 28 | // For messages to or from the sender or receiver platform, the special ids 29 | // 'sender-0' and 'receiver-0' can be used. 30 | // 31 | // For messages intended for all endpoints using a given channel, the 32 | // wildcard destination_id '*' can be used. 33 | required string source_id = 2; 34 | required string destination_id = 3; 35 | 36 | // This is the core multiplexing key. All messages are sent on a namespace 37 | // and endpoints sharing a channel listen on one or more namespaces. The 38 | // namespace defines the protocol and semantics of the message. 39 | required string namespace = 4; 40 | 41 | // Encoding and payload info follows. 42 | 43 | // What type of data do we have in this message. 44 | enum PayloadType { 45 | STRING = 0; 46 | BINARY = 1; 47 | } 48 | required PayloadType payload_type = 5; 49 | 50 | // Depending on payload_type, exactly one of the following optional fields 51 | // will always be set. 52 | optional string payload_utf8 = 6; 53 | optional bytes payload_binary = 7; 54 | } 55 | 56 | enum SignatureAlgorithm { 57 | UNSPECIFIED = 0; 58 | RSASSA_PKCS1v15 = 1; 59 | RSASSA_PSS = 2; 60 | } 61 | 62 | // Messages for authentication protocol between a sender and a receiver. 63 | message AuthChallenge { 64 | optional SignatureAlgorithm signature_algorithm = 1 65 | [default = RSASSA_PKCS1v15]; 66 | } 67 | 68 | message AuthResponse { 69 | required bytes signature = 1; 70 | required bytes client_auth_certificate = 2; 71 | repeated bytes intermediate_certificate = 3; 72 | optional SignatureAlgorithm signature_algorithm = 4 73 | [default = RSASSA_PKCS1v15]; 74 | } 75 | 76 | message AuthError { 77 | enum ErrorType { 78 | INTERNAL_ERROR = 0; 79 | NO_TLS = 1; // The underlying connection is not TLS 80 | SIGNATURE_ALGORITHM_UNAVAILABLE = 2; 81 | } 82 | required ErrorType error_type = 1; 83 | } 84 | 85 | message DeviceAuthMessage { 86 | // Request fields 87 | optional AuthChallenge challenge = 1; 88 | // Response fields 89 | optional AuthResponse response = 2; 90 | optional AuthError error = 3; 91 | } -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/ConnectionLostTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import org.junit.After; 19 | import org.junit.Before; 20 | import org.junit.Test; 21 | 22 | import java.io.IOException; 23 | import java.net.ConnectException; 24 | 25 | import static org.junit.Assert.assertNotNull; 26 | import static org.junit.Assert.assertNull; 27 | import static org.junit.Assert.assertTrue; 28 | 29 | public class ConnectionLostTest { 30 | MockedChromeCast chromeCastStub; 31 | ChromeCast cast = new ChromeCast("localhost"); 32 | 33 | @Before 34 | public void initMockedCast() throws Exception { 35 | chromeCastStub = new MockedChromeCast(); 36 | cast.connect(); 37 | chromeCastStub.close(); 38 | // ensure that chrome cast disconnected 39 | int retry = 0; 40 | while (cast.isConnected() && retry++ < 25) { 41 | Thread.sleep(50); 42 | } 43 | assertTrue("ChromeCast wasn't properly disconnected", retry < 25); 44 | } 45 | 46 | @Test(expected = ConnectException.class) 47 | public void testDisconnect() throws Exception { 48 | assertNull(cast.getStatus()); 49 | } 50 | 51 | @Test 52 | public void testReconnect() throws Exception { 53 | chromeCastStub = new MockedChromeCast(); 54 | assertNotNull(cast.getStatus()); 55 | } 56 | 57 | @After 58 | public void shutdown() throws IOException { 59 | if (cast.isConnected()) { 60 | cast.disconnect(); 61 | } 62 | chromeCastStub.close(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/CustomRequestTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | import com.fasterxml.jackson.databind.JsonNode; 20 | import org.junit.After; 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | 24 | import java.io.IOException; 25 | import java.security.GeneralSecurityException; 26 | 27 | import static org.junit.Assert.assertEquals; 28 | import static org.junit.Assert.assertNotNull; 29 | 30 | public class CustomRequestTest { 31 | MockedChromeCast chromeCastStub; 32 | ChromeCast cast = new ChromeCast("localhost"); 33 | 34 | private static class KioskStatusRequest implements Request { 35 | @JsonProperty 36 | final boolean status; 37 | 38 | private Long requestId; 39 | 40 | KioskStatusRequest() { 41 | status = true; 42 | } 43 | 44 | @Override 45 | public Long getRequestId() { 46 | return requestId; 47 | } 48 | 49 | @Override 50 | public void setRequestId(Long requestId) { 51 | this.requestId = requestId; 52 | } 53 | 54 | } 55 | 56 | private static class KioskStatusResponse implements Response { 57 | //current url 58 | @JsonProperty("url") 59 | String url; 60 | //current refresh rate 61 | @JsonProperty("refresh") 62 | int refresh; 63 | 64 | private Long requestId; 65 | 66 | public String getUrl() { 67 | return this.url; 68 | } 69 | 70 | public int getRefresh() { 71 | return this.refresh; 72 | } 73 | 74 | 75 | @Override 76 | public Long getRequestId() { 77 | return requestId; 78 | } 79 | 80 | @Override 81 | public void setRequestId(Long arg0) { 82 | requestId = arg0; 83 | } 84 | 85 | } 86 | 87 | @Before 88 | public void init() throws IOException, GeneralSecurityException { 89 | chromeCastStub = new MockedChromeCast(); 90 | cast.connect(); 91 | cast.launchApp("KIOSK"); 92 | } 93 | 94 | @After 95 | public void destroy() throws IOException { 96 | cast.disconnect(); 97 | chromeCastStub.close(); 98 | } 99 | 100 | @Test 101 | public void test() throws IOException { 102 | chromeCastStub.customHandler = new MockedChromeCast.CustomHandler() { 103 | @Override 104 | public Response handle(JsonNode json) { 105 | KioskStatusResponse response = new KioskStatusResponse(); 106 | response.url = "http://google.com"; 107 | response.refresh = 50; 108 | return response; 109 | } 110 | }; 111 | KioskStatusResponse response = cast.send("urn:x-cast:de.michaelkuerbis.kiosk", 112 | new KioskStatusRequest(), KioskStatusResponse.class); 113 | assertNotNull(response); 114 | assertEquals("http://google.com", response.getUrl()); 115 | assertEquals(50, response.getRefresh()); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/DeviceTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.databind.ObjectMapper; 19 | import org.junit.Test; 20 | 21 | import java.io.IOException; 22 | 23 | import static org.junit.Assert.assertEquals; 24 | import static org.junit.Assert.assertFalse; 25 | import static org.junit.Assert.assertNotNull; 26 | import static org.junit.Assert.assertNull; 27 | 28 | public class DeviceTest { 29 | final ObjectMapper jsonMapper = JacksonHelper.createJSONMapper(); 30 | 31 | @Test 32 | public void testDeviceAdded() throws IOException { 33 | final String jsonMSG = FixtureHelper.fixtureAsString("/device-added.json") 34 | .replaceFirst("\"type\"", "\"responseType\""); 35 | final StandardResponse.DeviceAdded response = jsonMapper.readValue(jsonMSG, StandardResponse.DeviceAdded.class); 36 | 37 | assertNotNull(response.device); 38 | Device device = response.device; 39 | assertEquals("Amplifier", device.name); 40 | assertEquals("123456", device.deviceId); 41 | assertEquals(4, device.capabilities); 42 | assertNotNull(device.volume); 43 | Volume volume = device.volume; 44 | assertEquals(0.24, volume.level, 0.001); 45 | assertFalse(volume.muted); 46 | assertNotNull(volume.increment); 47 | assertNotNull(volume.stepInterval); 48 | assertNull(volume.controlType); 49 | } 50 | 51 | @Test 52 | public void testDeviceRemoved() throws IOException { 53 | final String jsonMSG = FixtureHelper.fixtureAsString("/device-removed.json") 54 | .replaceFirst("\"type\"", "\"responseType\""); 55 | final StandardResponse.DeviceRemoved response = 56 | jsonMapper.readValue(jsonMSG, StandardResponse.DeviceRemoved.class); 57 | 58 | assertNotNull(response.deviceId); 59 | assertEquals("111111", response.deviceId); 60 | } 61 | 62 | @Test 63 | public void testDeviceUpdated() throws IOException { 64 | final String jsonMSG = FixtureHelper.fixtureAsString("/device-updated.json") 65 | .replaceFirst("\"type\"", "\"responseType\""); 66 | final StandardResponse.DeviceUpdated response = 67 | jsonMapper.readValue(jsonMSG, StandardResponse.DeviceUpdated.class); 68 | 69 | assertNotNull(response.device); 70 | Device device = response.device; 71 | assertEquals("Amplifier", device.name); 72 | assertEquals("654321", device.deviceId); 73 | assertEquals(4, device.capabilities); 74 | assertNotNull(device.volume); 75 | Volume volume = device.volume; 76 | assertEquals(0.35, volume.level, 0.001); 77 | assertFalse(volume.muted); 78 | assertNotNull(volume.increment); 79 | assertNotNull(volume.stepInterval); 80 | assertNull(volume.controlType); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/EventListenerHolderTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | import com.fasterxml.jackson.databind.JsonNode; 20 | import com.fasterxml.jackson.databind.ObjectMapper; 21 | import org.junit.Before; 22 | import org.junit.Test; 23 | import su.litvak.chromecast.api.v2.ChromeCastSpontaneousEvent.SpontaneousEventType; 24 | 25 | import java.io.IOException; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | 29 | import static org.junit.Assert.assertEquals; 30 | import static org.junit.Assert.assertNotNull; 31 | 32 | public class EventListenerHolderTest { 33 | private final ObjectMapper jsonMapper = JacksonHelper.createJSONMapper(); 34 | private List emittedEvents; 35 | private EventListenerHolder underTest; 36 | 37 | private static class CustomAppEvent { 38 | @JsonProperty 39 | public String responseType; 40 | @JsonProperty 41 | public long requestId; 42 | @JsonProperty 43 | public String event; 44 | 45 | @SuppressWarnings("unused") 46 | CustomAppEvent() { 47 | } 48 | 49 | CustomAppEvent(String responseType, long requestId, String event) { 50 | this.responseType = responseType; 51 | this.requestId = requestId; 52 | this.event = event; 53 | } 54 | 55 | @Override 56 | public int hashCode() { 57 | final int prime = 31; 58 | int result = 1; 59 | result = prime * result + ((event == null) ? 0 : event.hashCode()); 60 | result = prime * result + (int) (requestId ^ (requestId >>> 32)); 61 | result = prime * result 62 | + ((responseType == null) ? 0 : responseType.hashCode()); 63 | return result; 64 | } 65 | 66 | @Override 67 | public boolean equals(Object obj) { 68 | if (this == obj) { 69 | return true; 70 | } 71 | if (obj == null) { 72 | return false; 73 | } 74 | if (getClass() != obj.getClass()) { 75 | return false; 76 | } 77 | CustomAppEvent other = (CustomAppEvent) obj; 78 | if (event == null) { 79 | if (other.event != null) { 80 | return false; 81 | } 82 | } else if (!event.equals(other.event)) { 83 | return false; 84 | } 85 | if (requestId != other.requestId) { 86 | return false; 87 | } 88 | if (responseType == null) { 89 | if (other.responseType != null) { 90 | return false; 91 | } 92 | } else if (!responseType.equals(other.responseType)) { 93 | return false; 94 | } 95 | return true; 96 | } 97 | } 98 | 99 | @Before 100 | public void init() throws Exception { 101 | this.emittedEvents = new ArrayList(); 102 | this.underTest = new EventListenerHolder(); 103 | this.underTest.registerListener(new ChromeCastSpontaneousEventListener() { 104 | @Override 105 | public void spontaneousEventReceived(ChromeCastSpontaneousEvent event) { 106 | emittedEvents.add(event); 107 | } 108 | }); 109 | } 110 | 111 | @Test 112 | public void itHandlesMediaStatusEvent() throws Exception { 113 | final String json = FixtureHelper.fixtureAsString("/mediaStatus-chromecast-audio.json") 114 | .replaceFirst("\"type\"", "\"responseType\""); 115 | this.underTest.deliverEvent(jsonMapper.readTree(json)); 116 | 117 | ChromeCastSpontaneousEvent event = emittedEvents.get(0); 118 | 119 | assertEquals(SpontaneousEventType.MEDIA_STATUS, event.getType()); 120 | // Is it roughly what we passed in? More throughly tested in MediaStatusTest. 121 | assertEquals(15, event.getData(MediaStatus.class).supportedMediaCommands); 122 | 123 | assertEquals(1, emittedEvents.size()); 124 | } 125 | 126 | @Test 127 | public void itHandlesStatusEvent() throws Exception { 128 | Volume volume = new Volume(123f, false, 2f, Volume.DEFAULT_INCREMENT.doubleValue(), 129 | Volume.DEFAULT_CONTROL_TYPE); 130 | StandardResponse.Status status = new StandardResponse.Status(new Status(volume, null, false, false)); 131 | this.underTest.deliverEvent(jsonMapper.valueToTree(status)); 132 | 133 | ChromeCastSpontaneousEvent event = emittedEvents.get(0); 134 | 135 | assertEquals(SpontaneousEventType.STATUS, event.getType()); 136 | // Not trying to test everything, just that is basically what we passed in. 137 | assertEquals(volume, event.getData(Status.class).volume); 138 | 139 | assertEquals(1, emittedEvents.size()); 140 | } 141 | 142 | @Test 143 | public void itHandlesPlainAppEvent() throws Exception { 144 | final String namespace = "urn:x-cast:com.example.app"; 145 | final String message = "Sample message"; 146 | AppEvent appevent = new AppEvent(namespace, message); 147 | this.underTest.deliverAppEvent(appevent); 148 | 149 | ChromeCastSpontaneousEvent event = emittedEvents.get(0); 150 | 151 | assertEquals(SpontaneousEventType.APPEVENT, event.getType()); 152 | assertEquals(namespace, event.getData(AppEvent.class).namespace); 153 | assertEquals(message, event.getData(AppEvent.class).message); 154 | 155 | assertEquals(1, emittedEvents.size()); 156 | } 157 | 158 | @Test 159 | public void itHandlesJsonAppEvent() throws Exception { 160 | final String namespace = "urn:x-cast:com.example.app"; 161 | CustomAppEvent customAppEvent = new CustomAppEvent("MYEVENT", 3, "Sample message"); 162 | final String message = jsonMapper.writeValueAsString(customAppEvent); 163 | AppEvent appevent = new AppEvent(namespace, message); 164 | this.underTest.deliverAppEvent(appevent); 165 | 166 | ChromeCastSpontaneousEvent event = emittedEvents.get(0); 167 | 168 | assertEquals(SpontaneousEventType.APPEVENT, event.getType()); 169 | assertEquals(namespace, event.getData(AppEvent.class).namespace); 170 | 171 | // Check whether we received the same object 172 | CustomAppEvent responseEvent = jsonMapper.readValue( 173 | event.getData(AppEvent.class).message, CustomAppEvent.class); 174 | assertEquals(customAppEvent, responseEvent); 175 | 176 | assertEquals(1, emittedEvents.size()); 177 | } 178 | 179 | @Test 180 | public void itHandlesCloseBySenderEvent() throws Exception { 181 | StandardResponse.Close ev = new StandardResponse.Close(); 182 | this.underTest.deliverEvent(jsonMapper.valueToTree(ev)); 183 | 184 | ChromeCastSpontaneousEvent event = emittedEvents.get(0); 185 | 186 | assertEquals(SpontaneousEventType.CLOSE, event.getType()); 187 | 188 | assertEquals(1, emittedEvents.size()); 189 | } 190 | 191 | @Test 192 | public void itHandlesTimeTickEvent() throws Exception { 193 | final String jsonMSG = FixtureHelper.fixtureAsString("/timetick.json") 194 | .replaceFirst("\"type\"", "\"responseType\""); 195 | 196 | JsonNode jsonNode = jsonMapper.readTree(jsonMSG); 197 | underTest.deliverEvent(jsonNode); 198 | 199 | assertEquals(1, emittedEvents.size()); 200 | ChromeCastSpontaneousEvent event = emittedEvents.get(0); 201 | 202 | assertEquals(SpontaneousEventType.UNKNOWN, event.getType()); 203 | assertEquals(jsonMapper.writeValueAsString(jsonNode), 204 | jsonMapper.writeValueAsString(event.getData(JsonNode.class))); 205 | } 206 | 207 | @Test 208 | public void itHandlesSingleMediaStatusEvent() throws IOException { 209 | final String jsonMSG = FixtureHelper.fixtureAsString("/mediaStatus-single.json") 210 | .replaceFirst("\"type\"", "\"responseType\""); 211 | 212 | JsonNode jsonNode = jsonMapper.readTree(jsonMSG); 213 | underTest.deliverEvent(jsonNode); 214 | 215 | assertEquals(1, emittedEvents.size()); 216 | ChromeCastSpontaneousEvent event = emittedEvents.get(0); 217 | 218 | assertEquals(SpontaneousEventType.MEDIA_STATUS, event.getType()); 219 | MediaStatus mediaStatus = event.getData(MediaStatus.class); 220 | assertNotNull(mediaStatus); 221 | assertEquals(0, mediaStatus.mediaSessionId); 222 | assertNotNull(mediaStatus.media); 223 | assertEquals(Media.StreamType.NONE, mediaStatus.media.streamType); 224 | assertEquals(MediaStatus.PlayerState.IDLE, mediaStatus.playerState); 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/FixtureHelper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import java.io.BufferedReader; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.InputStreamReader; 22 | 23 | final class FixtureHelper { 24 | 25 | private FixtureHelper() { 26 | } 27 | 28 | static String fixtureAsString(final String res) throws IOException { 29 | final InputStream is = FixtureHelper.class.getResourceAsStream(res); 30 | try { 31 | final BufferedReader reader = new BufferedReader(new InputStreamReader(is)); 32 | final StringBuilder sb = new StringBuilder(); 33 | String line = null; 34 | while ((line = reader.readLine()) != null) { 35 | sb.append(line).append("\n"); 36 | } 37 | return sb.toString(); 38 | } finally { 39 | is.close(); 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/InterruptionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.databind.JsonNode; 19 | import org.hamcrest.CoreMatchers; 20 | import org.junit.After; 21 | import org.junit.Before; 22 | import org.junit.Rule; 23 | import org.junit.Test; 24 | import org.junit.rules.ExpectedException; 25 | 26 | import java.io.IOException; 27 | import java.util.concurrent.BrokenBarrierException; 28 | import java.util.concurrent.CyclicBarrier; 29 | import java.util.concurrent.ExecutionException; 30 | import java.util.concurrent.TimeoutException; 31 | import java.util.concurrent.atomic.AtomicReference; 32 | 33 | import static org.junit.Assert.assertEquals; 34 | import static org.junit.Assert.assertNotNull; 35 | import static org.junit.Assert.assertTrue; 36 | 37 | public class InterruptionTest { 38 | @Rule 39 | public ExpectedException thrown = ExpectedException.none(); 40 | 41 | MockedChromeCast chromeCastStub; 42 | ChromeCast cast = new ChromeCast("localhost"); 43 | CyclicBarrier barrier = new CyclicBarrier(2); 44 | 45 | static class Custom implements Request, Response { 46 | Long requestId; 47 | @Override 48 | public Long getRequestId() { 49 | return requestId; 50 | } 51 | 52 | @Override 53 | public void setRequestId(Long requestId) { 54 | this.requestId = requestId; 55 | } 56 | } 57 | 58 | @Before 59 | public void initMockedCast() throws Exception { 60 | chromeCastStub = new MockedChromeCast(); 61 | cast.connect(); 62 | cast.launchApp("abcd"); 63 | } 64 | 65 | @Test 66 | public void testInterrupt() throws IOException, ExecutionException, InterruptedException, BrokenBarrierException { 67 | chromeCastStub.customHandler = new MockedChromeCast.CustomHandler() { 68 | @Override 69 | public Response handle(JsonNode json) { 70 | try { 71 | Thread.sleep(10 * 1000); 72 | } catch (InterruptedException e) { 73 | e.printStackTrace(); 74 | } 75 | return new Custom(); 76 | } 77 | }; 78 | 79 | 80 | final AtomicReference exception = new AtomicReference(); 81 | Thread t = new Thread() { 82 | @Override 83 | public void run() { 84 | try { 85 | barrier.await(); 86 | cast.send("", new Custom()); 87 | } catch (IOException e) { 88 | exception.set(e); 89 | } catch (InterruptedException e) { 90 | e.printStackTrace(); 91 | } catch (BrokenBarrierException e) { 92 | e.printStackTrace(); 93 | } 94 | try { 95 | barrier.await(); 96 | } catch (InterruptedException e) { 97 | e.printStackTrace(); 98 | } catch (BrokenBarrierException e) { 99 | e.printStackTrace(); 100 | } 101 | } 102 | }; 103 | t.start(); 104 | barrier.await(); 105 | t.interrupt(); 106 | barrier.await(); 107 | assertNotNull(exception.get()); 108 | assertTrue(exception.get() instanceof ChromeCastException); 109 | assertTrue(exception.get().getCause() instanceof InterruptedException); 110 | assertEquals("Interrupted while waiting for response", exception.get().getMessage()); 111 | } 112 | 113 | @Test 114 | public void testTimeOut() throws IOException { 115 | chromeCastStub.customHandler = new MockedChromeCast.CustomHandler() { 116 | @Override 117 | public Response handle(JsonNode json) { 118 | try { 119 | Thread.sleep(10 * 1000); 120 | } catch (InterruptedException e) { 121 | e.printStackTrace(); 122 | } 123 | return new Custom(); 124 | } 125 | }; 126 | cast.setRequestTimeout(100L); 127 | thrown.expect(ChromeCastException.class); 128 | thrown.expectCause(CoreMatchers.isA(TimeoutException.class)); 129 | thrown.expectMessage("Waiting for response timed out"); 130 | cast.send("", new Custom(), Custom.class); 131 | } 132 | 133 | @After 134 | public void destroy() throws IOException { 135 | cast.disconnect(); 136 | chromeCastStub.close(); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/MediaStatusTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.databind.ObjectMapper; 19 | import org.junit.Test; 20 | import su.litvak.chromecast.api.v2.MediaStatus.PlayerState; 21 | import su.litvak.chromecast.api.v2.MediaStatus.RepeatMode; 22 | 23 | import java.io.IOException; 24 | import java.util.Collections; 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | 28 | import static org.junit.Assert.assertEquals; 29 | import static org.junit.Assert.assertNotNull; 30 | import static org.junit.Assert.assertNull; 31 | 32 | public class MediaStatusTest { 33 | final ObjectMapper jsonMapper = JacksonHelper.createJSONMapper(); 34 | 35 | @Test 36 | public void testDeserializationWithIdleReason() throws Exception { 37 | final StandardResponse.MediaStatus response = 38 | (StandardResponse.MediaStatus) jsonMapper.readValue(getClass() 39 | .getResourceAsStream("/mediaStatus-with-idleReason.json"), StandardResponse.class); 40 | assertEquals(1, response.statuses.length); 41 | MediaStatus mediaStatus = response.statuses[0]; 42 | assertEquals(MediaStatus.IdleReason.ERROR, mediaStatus.idleReason); 43 | } 44 | 45 | @Test 46 | public void testDeserializationWithoutIdleReason() throws Exception { 47 | final StandardResponse.MediaStatus response = 48 | (StandardResponse.MediaStatus) jsonMapper.readValue(getClass() 49 | .getResourceAsStream("/mediaStatus-without-idleReason.json"), StandardResponse.class); 50 | assertEquals(1, response.statuses.length); 51 | MediaStatus mediaStatus = response.statuses[0]; 52 | assertNull(mediaStatus.idleReason); 53 | } 54 | 55 | @Test 56 | public void testDeserializationWithChromeCastAudioFixture() throws Exception { 57 | final String jsonMSG = FixtureHelper.fixtureAsString("/mediaStatus-chromecast-audio.json") 58 | .replaceFirst("\"type\"", "\"responseType\""); 59 | final StandardResponse.MediaStatus response = 60 | (StandardResponse.MediaStatus) jsonMapper.readValue(jsonMSG, StandardResponse.class); 61 | assertEquals(1, response.statuses.length); 62 | final MediaStatus mediaStatus = response.statuses[0]; 63 | assertEquals((Integer) 1, mediaStatus.currentItemId); 64 | assertEquals(0f, mediaStatus.currentTime, 0f); 65 | 66 | final Media media = new Media("http://192.168.1.6:8192/audio-123-mp3", "audio/mpeg", 67 | 389.355102d, Media.StreamType.BUFFERED); 68 | 69 | final Map payload = new HashMap(); 70 | payload.put("thumb", null); 71 | payload.put("title", "Example Track Title"); 72 | final Map customData = new HashMap(); 73 | customData.put("payload", payload); 74 | assertEquals(Collections.singletonList(new Item(true, customData, 1, media)), mediaStatus.items); 75 | 76 | assertEquals(media, mediaStatus.media); 77 | assertEquals(Media.MetadataType.GENERIC, media.getMetadataType()); 78 | assertEquals(1, mediaStatus.mediaSessionId); 79 | assertEquals(1, mediaStatus.playbackRate); 80 | assertEquals(PlayerState.BUFFERING, mediaStatus.playerState); 81 | assertEquals(RepeatMode.REPEAT_OFF, mediaStatus.repeatMode); 82 | assertEquals(15, mediaStatus.supportedMediaCommands); 83 | assertEquals(new Volume(1f, false, Volume.DEFAULT_INCREMENT, 84 | Volume.DEFAULT_INCREMENT.doubleValue(), Volume.DEFAULT_CONTROL_TYPE), mediaStatus.volume); 85 | } 86 | 87 | @Test 88 | public void testDeserializationPandora() throws IOException { 89 | final StandardResponse.MediaStatus response = 90 | (StandardResponse.MediaStatus) jsonMapper.readValue(getClass() 91 | .getResourceAsStream("/mediaStatus-pandora.json"), StandardResponse.class); 92 | 93 | assertEquals(1, response.statuses.length); 94 | final MediaStatus mediaStatus = response.statuses[0]; 95 | assertNull(mediaStatus.currentItemId); 96 | assertEquals(16d, mediaStatus.currentTime, 0.1); 97 | assertEquals(7, mediaStatus.mediaSessionId); 98 | assertEquals(1, mediaStatus.playbackRate); 99 | assertEquals(PlayerState.PLAYING, mediaStatus.playerState); 100 | assertNull(mediaStatus.customData); 101 | assertNull(mediaStatus.items); 102 | assertNull(mediaStatus.preloadedItemId); 103 | 104 | assertEquals(new Volume(0.6999999f, false, 0.05f, null, null), mediaStatus.volume); 105 | 106 | assertNotNull(mediaStatus.media); 107 | Media media = mediaStatus.media; 108 | assertEquals(7, media.metadata.size()); 109 | assertEquals(Media.MetadataType.MUSIC_TRACK, media.getMetadataType()); 110 | assertEquals("http://audioURL", media.url); 111 | assertEquals(246d, media.duration, 0.1); 112 | assertEquals(Media.StreamType.BUFFERED, media.streamType); 113 | assertEquals("BUFFERED", media.contentType); 114 | assertNull(media.textTrackStyle); 115 | assertNull(media.tracks); 116 | assertEquals(1, media.customData.size()); 117 | assertNotNull(media.customData.get("status")); 118 | Map status = (Map) media.customData.get("status"); 119 | 120 | assertEquals(8, status.size()); 121 | assertEquals(2, status.get("state")); 122 | } 123 | 124 | @Test 125 | public void testDeserializationNoMetadataType() throws IOException { 126 | final StandardResponse.MediaStatus response = 127 | (StandardResponse.MediaStatus) jsonMapper.readValue(getClass() 128 | .getResourceAsStream("/mediaStatus-no-metadataType.json"), StandardResponse.class); 129 | 130 | final MediaStatus mediaStatus = response.statuses[0]; 131 | Media media = mediaStatus.media; 132 | assertEquals(Media.MetadataType.GENERIC, media.getMetadataType()); 133 | } 134 | 135 | @Test 136 | public void testDeserializationUnknownMetadataType() throws IOException { 137 | final StandardResponse.MediaStatus response = 138 | (StandardResponse.MediaStatus) jsonMapper.readValue(getClass() 139 | .getResourceAsStream("/mediaStatus-unknown-metadataType.json"), StandardResponse.class); 140 | 141 | final MediaStatus mediaStatus = response.statuses[0]; 142 | Media media = mediaStatus.media; 143 | assertEquals(Media.MetadataType.GENERIC, media.getMetadataType()); 144 | } 145 | 146 | @Test 147 | public void testDeserializationWithVideoInfo() throws IOException { 148 | final String jsonMSG = FixtureHelper.fixtureAsString("/mediaStatus-with-videoinfo.json") 149 | .replaceFirst("\"type\"", "\"responseType\""); 150 | final StandardResponse.MediaStatus response = 151 | (StandardResponse.MediaStatus) jsonMapper.readValue(jsonMSG, StandardResponse.class); 152 | assertEquals(1, response.statuses.length); 153 | } 154 | 155 | @Test 156 | public void testDeserializationAudioWithExtraStatus() throws IOException { 157 | final String jsonMSG = FixtureHelper.fixtureAsString("/mediaStatus-audio-with-extraStatus.json") 158 | .replaceFirst("\"type\"", "\"responseType\""); 159 | final StandardResponse.MediaStatus response = 160 | (StandardResponse.MediaStatus) jsonMapper.readValue(jsonMSG, StandardResponse.class); 161 | assertEquals(1, response.statuses.length); 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/MediaTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.databind.ObjectMapper; 19 | import org.junit.Test; 20 | import su.litvak.chromecast.api.v2.Media.StreamType; 21 | 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | 25 | import static org.hamcrest.CoreMatchers.not; 26 | import static org.hamcrest.MatcherAssert.assertThat; 27 | import static org.hamcrest.core.StringContains.containsString; 28 | 29 | public class MediaTest { 30 | final ObjectMapper jsonMapper = JacksonHelper.createJSONMapper(); 31 | 32 | @Test 33 | public void itIncludesOptionalFieldsWhenSet() throws Exception { 34 | Map customData = new HashMap(); 35 | customData.put("a", "b"); 36 | Map metadata = new HashMap(); 37 | metadata.put("1", "2"); 38 | Media m = new Media(null, null, 123.456d, StreamType.BUFFERED, customData, metadata, null, null); 39 | 40 | String json = jsonMapper.writeValueAsString(m); 41 | 42 | assertThat(json, containsString("\"duration\":123.456")); 43 | assertThat(json, containsString("\"streamType\":\"BUFFERED\"")); 44 | assertThat(json, containsString("\"customData\":{\"a\":\"b\"}")); 45 | assertThat(json, containsString("\"metadata\":{\"1\":\"2\"}")); 46 | } 47 | 48 | @Test 49 | public void itDoesNotContainOptionalFieldsWhenNotSet() throws Exception { 50 | Media m = new Media(null, null, null, null, null, null, null, null); 51 | 52 | String json = jsonMapper.writeValueAsString(m); 53 | 54 | assertThat(json, not(containsString("duration"))); 55 | assertThat(json, not(containsString("streamType"))); 56 | assertThat(json, not(containsString("customData"))); 57 | assertThat(json, not(containsString("metadata"))); 58 | assertThat(json, not(containsString("metadataType"))); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/MockedChromeCast.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.databind.JsonNode; 19 | import com.fasterxml.jackson.databind.ObjectMapper; 20 | import com.google.protobuf.MessageLite; 21 | import org.slf4j.Logger; 22 | import org.slf4j.LoggerFactory; 23 | import su.litvak.chromecast.api.v2.CastChannel.CastMessage; 24 | import su.litvak.chromecast.api.v2.CastChannel.CastMessage.PayloadType; 25 | import su.litvak.chromecast.api.v2.CastChannel.DeviceAuthMessage; 26 | 27 | import javax.net.ssl.KeyManagerFactory; 28 | import javax.net.ssl.SSLContext; 29 | import javax.net.ssl.TrustManager; 30 | 31 | import java.io.IOException; 32 | import java.io.InputStream; 33 | import java.net.ServerSocket; 34 | import java.net.Socket; 35 | import java.security.GeneralSecurityException; 36 | import java.security.KeyStore; 37 | import java.security.SecureRandom; 38 | import java.util.ArrayList; 39 | import java.util.Collections; 40 | import java.util.List; 41 | 42 | import static su.litvak.chromecast.api.v2.Util.fromArray; 43 | import static su.litvak.chromecast.api.v2.Util.toArray; 44 | 45 | final class MockedChromeCast { 46 | final Logger logger = LoggerFactory.getLogger(MockedChromeCast.class); 47 | 48 | final ServerSocket socket; 49 | final ClientThread clientThread; 50 | List runningApplications = new ArrayList(); 51 | CustomHandler customHandler; 52 | 53 | interface CustomHandler { 54 | Response handle(JsonNode json); 55 | } 56 | 57 | MockedChromeCast() throws IOException, GeneralSecurityException { 58 | SSLContext sc = SSLContext.getInstance("SSL"); 59 | KeyStore keyStore = KeyStore.getInstance("JKS"); 60 | keyStore.load(getClass().getResourceAsStream("/keystore.jks"), "changeit".toCharArray()); 61 | 62 | KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); 63 | keyManagerFactory.init(keyStore, "changeit".toCharArray()); 64 | 65 | sc.init(keyManagerFactory.getKeyManagers(), new TrustManager[] {new X509TrustAllManager()}, new SecureRandom()); 66 | socket = sc.getServerSocketFactory().createServerSocket(8009); 67 | 68 | clientThread = new ClientThread(); 69 | clientThread.start(); 70 | } 71 | 72 | class ClientThread extends Thread { 73 | volatile boolean stop; 74 | Socket clientSocket; 75 | ObjectMapper jsonMapper = JacksonHelper.createJSONMapper(); 76 | 77 | @Override 78 | public void run() { 79 | try { 80 | clientSocket = socket.accept(); 81 | while (!stop) { 82 | handle(read(clientSocket)); 83 | } 84 | } catch (IOException ioex) { 85 | logger.warn("Error while handling: {}", ioex.toString()); 86 | } finally { 87 | if (clientSocket != null) { 88 | try { 89 | clientSocket.close(); 90 | } catch (IOException ioex) { 91 | ioex.printStackTrace(); 92 | } 93 | } 94 | } 95 | } 96 | 97 | void handle(CastMessage message) throws IOException { 98 | logger.info("Received message: "); 99 | logger.info(" sourceId: " + message.getSourceId()); 100 | logger.info(" destinationId: " + message.getDestinationId()); 101 | logger.info(" namespace: " + message.getNamespace()); 102 | logger.info(" payloadType: " + message.getPayloadType()); 103 | if (message.getPayloadType() == PayloadType.STRING) { 104 | logger.info(" payload: " + message.getPayloadUtf8()); 105 | } 106 | 107 | if (message.getPayloadType() == PayloadType.BINARY) { 108 | MessageLite response = handleBinary(DeviceAuthMessage.parseFrom(message.getPayloadBinary())); 109 | logger.info("Sending response message: "); 110 | logger.info(" sourceId: " + message.getDestinationId()); 111 | logger.info(" destinationId: " + message.getSourceId()); 112 | logger.info(" namespace: " + message.getNamespace()); 113 | logger.info(" payloadType: " + PayloadType.BINARY); 114 | write(clientSocket, 115 | CastMessage.newBuilder() 116 | .setProtocolVersion(message.getProtocolVersion()) 117 | .setSourceId(message.getDestinationId()) 118 | .setDestinationId(message.getSourceId()) 119 | .setNamespace(message.getNamespace()) 120 | .setPayloadType(PayloadType.BINARY) 121 | .setPayloadBinary(response.toByteString()) 122 | .build()); 123 | } else { 124 | JsonNode json = jsonMapper.readTree(message.getPayloadUtf8()); 125 | Response response = null; 126 | if (json.has("type")) { 127 | StandardMessage standardMessage = jsonMapper.readValue(message.getPayloadUtf8(), 128 | StandardMessage.class); 129 | response = handleJSON(standardMessage); 130 | } else { 131 | response = handleCustom(json); 132 | } 133 | 134 | if (response != null) { 135 | if (json.has("requestId")) { 136 | response.setRequestId(json.get("requestId").asLong()); 137 | } 138 | 139 | logger.info("Sending response message: "); 140 | logger.info(" sourceId: " + message.getDestinationId()); 141 | logger.info(" destinationId: " + message.getSourceId()); 142 | logger.info(" namespace: " + message.getNamespace()); 143 | logger.info(" payloadType: " + CastMessage.PayloadType.STRING); 144 | logger.info(" payload: " + jsonMapper.writeValueAsString(response)); 145 | write(clientSocket, 146 | CastMessage.newBuilder() 147 | .setProtocolVersion(message.getProtocolVersion()) 148 | .setSourceId(message.getDestinationId()) 149 | .setDestinationId(message.getSourceId()) 150 | .setNamespace(message.getNamespace()) 151 | .setPayloadType(CastMessage.PayloadType.STRING) 152 | .setPayloadUtf8(jsonMapper.writeValueAsString(response)) 153 | .build()); 154 | } 155 | } 156 | } 157 | 158 | MessageLite handleBinary(DeviceAuthMessage message) throws IOException { 159 | return message; 160 | } 161 | 162 | Response handleJSON(Message message) { 163 | if (message instanceof StandardMessage.Ping) { 164 | return new StandardResponse.Pong(); 165 | } else if (message instanceof StandardRequest.Status) { 166 | return new StandardResponse.Status(status()); 167 | } else if (message instanceof StandardRequest.Launch) { 168 | StandardRequest.Launch launch = (StandardRequest.Launch) message; 169 | runningApplications.add(new Application(launch.appId, "iconUrl", launch.appId, "SESSION_ID", "", 170 | false, false, "", Collections.emptyList())); 171 | StandardResponse response = new StandardResponse.Status(status()); 172 | response.setRequestId(launch.getRequestId()); 173 | return response; 174 | } 175 | return null; 176 | } 177 | 178 | Status status() { 179 | return new Status(new Volume(1f, false, Volume.DEFAULT_INCREMENT, 180 | Volume.DEFAULT_INCREMENT.doubleValue(), Volume.DEFAULT_CONTROL_TYPE), 181 | runningApplications, false, true); 182 | } 183 | 184 | Response handleCustom(JsonNode json) { 185 | if (customHandler == null) { 186 | logger.info("No custom handler set"); 187 | return null; 188 | } else { 189 | return customHandler.handle(json); 190 | } 191 | } 192 | 193 | CastMessage read(Socket mySocket) throws IOException { 194 | InputStream is = mySocket.getInputStream(); 195 | byte[] buf = new byte[4]; 196 | 197 | int read = 0; 198 | while (read < buf.length) { 199 | int nextByte = is.read(); 200 | if (nextByte == -1) { 201 | throw new ChromeCastException("Remote socket was closed"); 202 | } 203 | buf[read++] = (byte) nextByte; 204 | } 205 | 206 | int size = fromArray(buf); 207 | buf = new byte[size]; 208 | read = 0; 209 | while (read < size) { 210 | int nowRead = is.read(buf, read, buf.length - read); 211 | if (nowRead == -1) { 212 | throw new ChromeCastException("Remote socket was closed"); 213 | } 214 | read += nowRead; 215 | } 216 | 217 | return CastMessage.parseFrom(buf); 218 | } 219 | 220 | void write(Socket mySocket, CastMessage message) throws IOException { 221 | mySocket.getOutputStream().write(toArray(message.getSerializedSize())); 222 | message.writeTo(mySocket.getOutputStream()); 223 | } 224 | } 225 | 226 | void close() throws IOException { 227 | clientThread.stop = true; 228 | this.socket.close(); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/MultizoneStatusTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.databind.ObjectMapper; 19 | import org.junit.Test; 20 | 21 | import java.io.IOException; 22 | 23 | import static org.junit.Assert.assertEquals; 24 | import static org.junit.Assert.assertFalse; 25 | import static org.junit.Assert.assertNotNull; 26 | 27 | public class MultizoneStatusTest { 28 | final ObjectMapper jsonMapper = JacksonHelper.createJSONMapper(); 29 | 30 | @Test 31 | public void testStandard() throws IOException { 32 | final String jsonMSG = FixtureHelper.fixtureAsString("/multizoneStatus.json") 33 | .replaceFirst("\"type\"", "\"responseType\""); 34 | final StandardResponse.MultizoneStatus response = 35 | (StandardResponse.MultizoneStatus) jsonMapper.readValue(jsonMSG, StandardResponse.class); 36 | assertNotNull(response.status); 37 | assertEquals(1, response.status.devices.length); 38 | assertFalse(response.status.isMultichannel); 39 | assertEquals("Living Room speaker", response.status.devices[0].name); 40 | assertEquals(196612, response.status.devices[0].capabilities); 41 | assertNotNull(response.status.devices[0].volume); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/StatusTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import com.fasterxml.jackson.databind.ObjectMapper; 19 | import org.junit.Test; 20 | 21 | import static org.junit.Assert.assertEquals; 22 | import static org.junit.Assert.assertFalse; 23 | import static org.junit.Assert.assertNotNull; 24 | import static org.junit.Assert.assertNull; 25 | import static org.junit.Assert.assertTrue; 26 | 27 | public class StatusTest { 28 | 29 | final ObjectMapper jsonMapper = JacksonHelper.createJSONMapper(); 30 | 31 | @Test 32 | public void testDeserializationBackdrop118() throws Exception { 33 | final String jsonMSG = FixtureHelper.fixtureAsString("/status-backdrop-1.18.json") 34 | .replaceFirst("\"type\"", "\"responseType\""); 35 | final StandardResponse.Status response = jsonMapper.readValue(jsonMSG, StandardResponse.Status.class); 36 | 37 | Status status = response.status; 38 | assertNotNull(status); 39 | assertTrue(status.activeInput); 40 | assertFalse(status.standBy); 41 | 42 | assertEquals(1, status.applications.size()); 43 | Application app = status.getRunningApp(); 44 | assertFalse(app.isIdleScreen); 45 | 46 | Volume volume = status.volume; 47 | assertNotNull(volume); 48 | assertEquals(1.0, volume.level, 0.1); 49 | assertFalse(volume.muted); 50 | assertNull(volume.controlType); 51 | assertEquals(Volume.DEFAULT_INCREMENT, volume.increment, 0.001); 52 | assertEquals(Volume.DEFAULT_INCREMENT, volume.stepInterval, 0.001); 53 | } 54 | 55 | @Test 56 | public void testDeserializationBackdrop119() throws Exception { 57 | final String jsonMSG = FixtureHelper.fixtureAsString("/status-backdrop-1.19.json") 58 | .replaceFirst("\"type\"", "\"responseType\""); 59 | final StandardResponse.Status response = jsonMapper.readValue(jsonMSG, StandardResponse.Status.class); 60 | 61 | Status status = response.status; 62 | assertNotNull(status); 63 | assertFalse(status.activeInput); 64 | assertFalse(status.standBy); 65 | 66 | assertEquals(1, status.applications.size()); 67 | Application app = status.getRunningApp(); 68 | assertTrue(app.isIdleScreen); 69 | 70 | Volume volume = status.volume; 71 | assertNotNull(volume); 72 | assertEquals(1.0, volume.level, 0.1); 73 | assertFalse(volume.muted); 74 | assertEquals("attenuation", volume.controlType); 75 | assertEquals(Volume.DEFAULT_INCREMENT, volume.increment, 0.001); 76 | assertEquals(0.04, volume.stepInterval, 0.001); 77 | } 78 | 79 | @Test 80 | public void testDeserializationBackdrop128() throws Exception { 81 | final String jsonMSG = FixtureHelper.fixtureAsString("/status-backdrop-1.28.json") 82 | .replaceFirst("\"type\"", "\"responseType\""); 83 | final StandardResponse.Status response = jsonMapper.readValue(jsonMSG, StandardResponse.Status.class); 84 | 85 | Status status = response.status; 86 | assertNotNull(status); 87 | assertFalse(status.activeInput); 88 | assertFalse(status.standBy); 89 | 90 | assertEquals(1, status.applications.size()); 91 | Application app = status.getRunningApp(); 92 | assertTrue(app.isIdleScreen); 93 | assertFalse(app.launchedFromCloud); 94 | 95 | Volume volume = status.volume; 96 | assertNotNull(volume); 97 | assertEquals(1.0, volume.level, 0.1); 98 | assertFalse(volume.muted); 99 | assertEquals("attenuation", volume.controlType); 100 | assertEquals(Volume.DEFAULT_INCREMENT, volume.increment, 0.001); 101 | assertEquals(0.05, volume.stepInterval, 0.001); 102 | } 103 | 104 | @Test 105 | public void testDeserializationChromeMirroring() throws Exception { 106 | final String jsonMSG = FixtureHelper.fixtureAsString("/status-chrome-mirroring-1.28.json") 107 | .replaceFirst("\"type\"", "\"responseType\""); 108 | final StandardResponse.Status response = jsonMapper.readValue(jsonMSG, StandardResponse.Status.class); 109 | 110 | Status status = response.status; 111 | assertNotNull(status); 112 | assertFalse(status.activeInput); 113 | assertFalse(status.standBy); 114 | 115 | assertEquals(1, status.applications.size()); 116 | Application app = status.getRunningApp(); 117 | assertFalse(app.isIdleScreen); 118 | assertFalse(app.launchedFromCloud); 119 | 120 | Volume volume = status.volume; 121 | assertNotNull(volume); 122 | assertEquals(1.0, volume.level, 0.1); 123 | assertFalse(volume.muted); 124 | assertEquals("attenuation", volume.controlType); 125 | assertEquals(Volume.DEFAULT_INCREMENT, volume.increment, 0.001); 126 | assertEquals(0.05, volume.stepInterval, 0.001); 127 | } 128 | 129 | @Test 130 | public void testDeserializationSpotify() throws Exception { 131 | final String jsonMSG = FixtureHelper.fixtureAsString("/status-spotify.json") 132 | .replaceFirst("\"type\"", "\"responseType\""); 133 | final StandardResponse.Status response = jsonMapper.readValue(jsonMSG, StandardResponse.Status.class); 134 | 135 | Status status = response.status; 136 | assertNotNull(status); 137 | assertFalse(status.activeInput); 138 | assertFalse(status.standBy); 139 | 140 | assertEquals(1, status.applications.size()); 141 | Application app = status.getRunningApp(); 142 | assertFalse(app.isIdleScreen); 143 | assertFalse(app.launchedFromCloud); 144 | assertEquals("CC32E753", app.id); 145 | assertEquals("Spotify", app.name); 146 | assertEquals("https://lh3.googleusercontent.com/HOX9yqNu6y87Chb1lHYqhK" 147 | + "VTQW43oFAFFe2ojx94yCLh0yMzgygTrM0RweAexApRWqq6UahgrWYimVgK", app.iconUrl); 148 | assertEquals(6, app.namespaces.size()); 149 | assertEquals("urn:x-cast:com.google.cast.debugoverlay", app.namespaces.get(0).name); 150 | assertEquals("urn:x-cast:com.google.cast.cac", app.namespaces.get(1).name); 151 | assertEquals("urn:x-cast:com.spotify.chromecast.secure.v1", app.namespaces.get(2).name); 152 | assertEquals("urn:x-cast:com.google.cast.test", app.namespaces.get(3).name); 153 | assertEquals("urn:x-cast:com.google.cast.broadcast", app.namespaces.get(4).name); 154 | assertEquals("urn:x-cast:com.google.cast.media", app.namespaces.get(5).name); 155 | assertEquals("7fb71850-b38b-43bb-967e-e2c76b6d0990", app.sessionId); 156 | assertEquals("Spotify", app.statusText); 157 | assertEquals("7fb71850-b38b-43bb-967e-e2c76b6d0990", app.transportId); 158 | 159 | Volume volume = status.volume; 160 | assertNotNull(volume); 161 | assertEquals(0.2258118838071823, volume.level, 0.001); 162 | assertFalse(volume.muted); 163 | assertEquals("master", volume.controlType); 164 | assertEquals(Volume.DEFAULT_INCREMENT, volume.increment, 0.001); 165 | assertEquals(0.019999999552965164, volume.stepInterval, 0.001); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/test/java/su/litvak/chromecast/api/v2/UtilTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Vitaly Litvak (vitavaque@gmail.com) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package su.litvak.chromecast.api.v2; 17 | 18 | import org.junit.Test; 19 | 20 | import java.io.IOException; 21 | 22 | import static org.junit.Assert.assertEquals; 23 | import static su.litvak.chromecast.api.v2.Util.getMediaTitle; 24 | 25 | public class UtilTest { 26 | @Test 27 | public void testMediaTitle() throws IOException { 28 | assertEquals("stream", getMediaTitle("http://xxx.yyy.com:8054/stream")); 29 | assertEquals("stream", getMediaTitle("http://xxx.yyy.com/stream")); 30 | assertEquals("stream", getMediaTitle("http://zzz.aaa.com:8054/stream.mp3")); 31 | assertEquals("stream", getMediaTitle("http://zzz.aaa.com/stream.mp3")); 32 | assertEquals("stream.abc", getMediaTitle("http://zzz.aaa.com/stream.abc.mp3")); 33 | assertEquals("http://zzz.aaa.com/", getMediaTitle("http://zzz.aaa.com/")); 34 | assertEquals("http://zzz.aaa.com", getMediaTitle("http://zzz.aaa.com")); 35 | assertEquals("BigBuckBunny", 36 | getMediaTitle("http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/test/resources/device-added.json: -------------------------------------------------------------------------------- 1 | {"device":{"capabilities":4,"deviceId":"123456","name":"Amplifier","volume":{"level":0.24,"muted":false}},"requestId":0,"type":"DEVICE_ADDED"} -------------------------------------------------------------------------------- /src/test/resources/device-removed.json: -------------------------------------------------------------------------------- 1 | {"deviceId":"111111","requestId":0,"type":"DEVICE_REMOVED"} -------------------------------------------------------------------------------- /src/test/resources/device-updated.json: -------------------------------------------------------------------------------- 1 | {"device":{"capabilities":4,"deviceId":"654321","name":"Amplifier","volume":{"level":0.35,"muted":false}},"requestId":0,"type":"DEVICE_UPDATED"} -------------------------------------------------------------------------------- /src/test/resources/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitalidze/chromecast-java-api-v2/c1358030b1b6afa11dc2a83a0049783e7b5bef7e/src/test/resources/keystore.jks -------------------------------------------------------------------------------- /src/test/resources/mediaStatus-audio-with-extraStatus.json: -------------------------------------------------------------------------------- 1 | {"type":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"IDLE","currentTime":0,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"media":{"contentId":"http://192.168.2.139:9080/audio/99fd6998-aa4d-4764-9b41-c6869dcfc85f.mp3","contentType":null},"currentItemId":1,"extendedStatus":{"playerState":"LOADING","media":{"contentId":"http://192.168.2.139:9080/audio/99fd6998-aa4d-4764-9b41-c6869dcfc85f.mp3","contentType":null}},"repeatMode":"REPEAT_OFF"}],"requestId":0} -------------------------------------------------------------------------------- /src/test/resources/mediaStatus-chromecast-audio.json: -------------------------------------------------------------------------------- 1 | { 2 | "requestId": 3, 3 | "status": [ 4 | { 5 | "currentItemId": 1, 6 | "currentTime": 0, 7 | "items": [ 8 | { 9 | "autoplay": true, 10 | "customData": { 11 | "payload": { 12 | "thumb": null, 13 | "title:": "Example Track Title" 14 | } 15 | }, 16 | "itemId": 1, 17 | "media": { 18 | "contentId": "http://192.168.1.6:8192/audio-123-mp3", 19 | "contentType": "audio/mpeg", 20 | "duration": 389.355102, 21 | "streamType": "buffered" 22 | } 23 | } 24 | ], 25 | "media": { 26 | "contentId": "http://192.168.1.6:8192/audio-123-mp3", 27 | "contentType": "audio/mpeg", 28 | "duration": 389.355102, 29 | "streamType": "buffered" 30 | }, 31 | "mediaSessionId": 1, 32 | "playbackRate": 1, 33 | "playerState": "BUFFERING", 34 | "repeatMode": "REPEAT_OFF", 35 | "supportedMediaCommands": 15, 36 | "volume": { 37 | "level": 1, 38 | "muted": false, 39 | "stepInterval": 0.05000000074505806, 40 | "controlType":"attenuation" 41 | } 42 | } 43 | ], 44 | "type": "MEDIA_STATUS" 45 | } -------------------------------------------------------------------------------- /src/test/resources/mediaStatus-no-metadataType.json: -------------------------------------------------------------------------------- 1 | {"requestId":1, 2 | "responseType":"MEDIA_STATUS", 3 | "status":[ 4 | {"currentTime":16, 5 | "playbackRate":1, 6 | "supportedMediaCommands":29, 7 | "playerState":"PLAYING", 8 | "media": 9 | {"metadata": 10 | { 11 | "albumName":"The Album", 12 | "title":"And We Danced", 13 | "albumArtist":"The Artist", 14 | "artist":"The Artist", 15 | "images": [ 16 | {"url": "http://lh3.googleusercontent.com/UirYk5XiPVHW2HHRtoVlvHF10_Of8VtYU9DL18qwFsFodXd3hXo60yX1BfV5up5ClCKhgZvLPUY"} 17 | ], 18 | "releaseDate":"1994-11-05T13:15:30Z"}, 19 | "contentId":"http://audioURL", 20 | "duration":246, 21 | "streamType":"BUFFERED", 22 | "contentType":"BUFFERED", 23 | "customData": 24 | {"status": 25 | {"state":2, 26 | "content_id":"2308730880869724752", 27 | "content_info": 28 | {"stationName":"Macklemore & Ryan Lewis Radio", 29 | "stationId":"2308730880869724752", 30 | "stationToken":"2308730880869724752", 31 | "isQuickMix":false, 32 | "supportImpressionTargeting":true, 33 | "onePlaylist":false, 34 | "userId":"70709840", 35 | "casterName":"Steven Feldman", 36 | "songName":"And We Danced", 37 | "artistName":"Macklemore", 38 | "albumName":"The Unplanned Mixtape", 39 | "artUrl":"http://art.jpg","trackToken":"ttt","songRating":0,"songDetailUrl":"http://songDetail", 40 | "allowFeedback":true, 41 | "artistExplorerUrl":"http://artist", 42 | "audioUrlMap": 43 | {"highQuality": 44 | {"audioUrl":"http://audio"} 45 | } 46 | }, 47 | "position":16, 48 | "current_time":16, 49 | "current":16, 50 | "duration":246, 51 | "volume": 52 | {"level":0.6999999284744263, 53 | "muted":false, 54 | "increment":0.05} 55 | } 56 | } 57 | }, 58 | "mediaSessionId":7, 59 | "volume":{"level":0.6999999284744263,"muted":false,"increment":0.05} 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /src/test/resources/mediaStatus-pandora.json: -------------------------------------------------------------------------------- 1 | {"requestId":1, 2 | "responseType":"MEDIA_STATUS", 3 | "status":[ 4 | {"currentTime":16, 5 | "playbackRate":1, 6 | "supportedMediaCommands":29, 7 | "playerState":"PLAYING", 8 | "media": 9 | {"metadata": 10 | {"metadataType":3, 11 | "albumName":"The Album", 12 | "title":"And We Danced", 13 | "albumArtist":"The Artist", 14 | "artist":"The Artist", 15 | "images": [ 16 | {"url": "http://lh3.googleusercontent.com/UirYk5XiPVHW2HHRtoVlvHF10_Of8VtYU9DL18qwFsFodXd3hXo60yX1BfV5up5ClCKhgZvLPUY"} 17 | ], 18 | "releaseDate":"1994-11-05T13:15:30Z"}, 19 | "contentId":"http://audioURL", 20 | "duration":246, 21 | "streamType":"BUFFERED", 22 | "contentType":"BUFFERED", 23 | "customData": 24 | {"status": 25 | {"state":2, 26 | "content_id":"2308730880869724752", 27 | "content_info": 28 | {"stationName":"Macklemore & Ryan Lewis Radio", 29 | "stationId":"2308730880869724752", 30 | "stationToken":"2308730880869724752", 31 | "isQuickMix":false, 32 | "supportImpressionTargeting":true, 33 | "onePlaylist":false, 34 | "userId":"70709840", 35 | "casterName":"Steven Feldman", 36 | "songName":"And We Danced", 37 | "artistName":"Macklemore", 38 | "albumName":"The Unplanned Mixtape", 39 | "artUrl":"http://art.jpg","trackToken":"ttt","songRating":0,"songDetailUrl":"http://songDetail", 40 | "allowFeedback":true, 41 | "artistExplorerUrl":"http://artist", 42 | "audioUrlMap": 43 | {"highQuality": 44 | {"audioUrl":"http://audio"} 45 | } 46 | }, 47 | "position":16, 48 | "current_time":16, 49 | "current":16, 50 | "duration":246, 51 | "volume": 52 | {"level":0.6999999284744263, 53 | "muted":false, 54 | "increment":0.05} 55 | } 56 | } 57 | }, 58 | "mediaSessionId":7, 59 | "volume":{"level":0.6999999284744263,"muted":false,"increment":0.05} 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /src/test/resources/mediaStatus-single.json: -------------------------------------------------------------------------------- 1 | {"type":"MEDIA_STATUS","mediaSessionId":0,"media":{"contentId":"","streamType":"NONE","contentType":"","metadata":{"metadataType":0,"title":"We Take Your Calls","images":[{"url":""}]}},"playbackRate":1,"playerState":"IDLE","currentTime":0,"supportedMediaCommands":0,"volume":{}} -------------------------------------------------------------------------------- /src/test/resources/mediaStatus-unknown-metadataType.json: -------------------------------------------------------------------------------- 1 | {"requestId":1, 2 | "responseType":"MEDIA_STATUS", 3 | "status":[ 4 | {"currentTime":16, 5 | "playbackRate":1, 6 | "supportedMediaCommands":29, 7 | "playerState":"PLAYING", 8 | "media": 9 | {"metadata": 10 | {"metadataType":103, 11 | "albumName":"The Album", 12 | "title":"And We Danced", 13 | "albumArtist":"The Artist", 14 | "artist":"The Artist", 15 | "images": [ 16 | {"url": "http://lh3.googleusercontent.com/UirYk5XiPVHW2HHRtoVlvHF10_Of8VtYU9DL18qwFsFodXd3hXo60yX1BfV5up5ClCKhgZvLPUY"} 17 | ], 18 | "releaseDate":"1994-11-05T13:15:30Z"}, 19 | "contentId":"http://audioURL", 20 | "duration":246, 21 | "streamType":"BUFFERED", 22 | "contentType":"BUFFERED", 23 | "customData": 24 | {"status": 25 | {"state":2, 26 | "content_id":"2308730880869724752", 27 | "content_info": 28 | {"stationName":"Macklemore & Ryan Lewis Radio", 29 | "stationId":"2308730880869724752", 30 | "stationToken":"2308730880869724752", 31 | "isQuickMix":false, 32 | "supportImpressionTargeting":true, 33 | "onePlaylist":false, 34 | "userId":"70709840", 35 | "casterName":"Steven Feldman", 36 | "songName":"And We Danced", 37 | "artistName":"Macklemore", 38 | "albumName":"The Unplanned Mixtape", 39 | "artUrl":"http://art.jpg","trackToken":"ttt","songRating":0,"songDetailUrl":"http://songDetail", 40 | "allowFeedback":true, 41 | "artistExplorerUrl":"http://artist", 42 | "audioUrlMap": 43 | {"highQuality": 44 | {"audioUrl":"http://audio"} 45 | } 46 | }, 47 | "position":16, 48 | "current_time":16, 49 | "current":16, 50 | "duration":246, 51 | "volume": 52 | {"level":0.6999999284744263, 53 | "muted":false, 54 | "increment":0.05} 55 | } 56 | } 57 | }, 58 | "mediaSessionId":7, 59 | "volume":{"level":0.6999999284744263,"muted":false,"increment":0.05} 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /src/test/resources/mediaStatus-with-idleReason.json: -------------------------------------------------------------------------------- 1 | {"responseType":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"IDLE","currentTime":0,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"media":{"contentId":"/public/Videos/Movies/FileB.mp4","contentType":"video/transcode","streamType":"buffered","duration":null},"idleReason":"ERROR"}],"requestId":28} 2 | -------------------------------------------------------------------------------- /src/test/resources/mediaStatus-with-videoinfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "type":"MEDIA_STATUS", 3 | "status":[ 4 | { 5 | "mediaSessionId":1, 6 | "playbackRate":1, 7 | "playerState":"BUFFERING", 8 | "currentTime":0, 9 | "supportedMediaCommands":15, 10 | "volume":{ 11 | "level":1, 12 | "muted":false 13 | }, 14 | "videoInfo":{ 15 | "width":1280, 16 | "height":720, 17 | "hdrType":"sdr" 18 | }, 19 | "media":{ 20 | "contentId":"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", 21 | "contentType":"video/mp4", 22 | "duration":596.474195 23 | }, 24 | "currentItemId":1, 25 | "items":[ 26 | { 27 | "itemId":1, 28 | "media":{ 29 | "contentId":"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4", 30 | "contentType":"video/mp4", 31 | "duration":596.474195 32 | }, 33 | "autoplay":true, 34 | "customData":{ 35 | "payload":{ 36 | "title:":"Big Buck Bunny", 37 | "thumb":"images/BigBuckBunny.jpg" 38 | } 39 | } 40 | } 41 | ], 42 | "repeatMode":"REPEAT_OFF" 43 | } 44 | ], 45 | "requestId":7 46 | } -------------------------------------------------------------------------------- /src/test/resources/mediaStatus-without-idleReason.json: -------------------------------------------------------------------------------- 1 | {"responseType":"MEDIA_STATUS","status":[{"mediaSessionId":1,"playbackRate":1,"playerState":"IDLE","currentTime":0,"supportedMediaCommands":15,"volume":{"level":1,"muted":false},"media":{"contentId":"/public/Videos/Movies/FileB.mp4","contentType":"video/transcode","streamType":"buffered","duration":null}}],"requestId":28} 2 | -------------------------------------------------------------------------------- /src/test/resources/multizoneStatus.json: -------------------------------------------------------------------------------- 1 | {"requestId":0,"status":{"devices":[{"capabilities":196612,"deviceId":"123456-11c9-8822-cbde-114234fabd","name":"Living Room speaker","volume":{"level":0.30000001192092896,"muted":false}}],"isMultichannel":false},"type":"MULTIZONE_STATUS"} -------------------------------------------------------------------------------- /src/test/resources/simplelogger.properties: -------------------------------------------------------------------------------- 1 | org.slf4j.simpleLogger.showShortLogName = true 2 | -------------------------------------------------------------------------------- /src/test/resources/status-backdrop-1.18.json: -------------------------------------------------------------------------------- 1 | {"requestId":1,"status":{"applications":[{"appId":"E8C28D3C","displayName":"Backdrop","namespaces":[{"name":"urn:x-cast:com.google.cast.sse"}],"sessionId":"1CAFD4C1-39BD...","statusText":"","transportId":"web-3"}],"isActiveInput":true,"isStandBy":false,"volume":{"level":1.0,"muted":false}},"type":"RECEIVER_STATUS"} -------------------------------------------------------------------------------- /src/test/resources/status-backdrop-1.19.json: -------------------------------------------------------------------------------- 1 | {"requestId":1,"status":{"applications":[{"appId":"E8C28D3C","displayName":"Backdrop","isIdleScreen":true,"namespaces":[{"name":"urn:x-cast:com.google.cast.sse"}],"sessionId":"E5D50197-E550...","statusText":"","transportId":"web-5"}],"volume":{"controlType":"attenuation","level":1.0,"muted":false,"stepInterval":0.04}},"type":"RECEIVER_STATUS"} -------------------------------------------------------------------------------- /src/test/resources/status-backdrop-1.28.json: -------------------------------------------------------------------------------- 1 | {"requestId":1,"status":{"applications":[{"appId":"E8C28D3C","displayName":"Backdrop","isIdleScreen":true,"launchedFromCloud":false,"namespaces":[{"name":"urn:x-cast:com.google.cast.debugoverlay"},{"name":"urn:x-cast:com.google.cast.cac"},{"name":"urn:x-cast:com.google.cast.sse"}],"sessionId":"...","statusText":"","transportId":"..."}],"volume":{"controlType":"attenuation","level":1.0,"muted":false,"stepInterval":0.05000000074505806}},"type":"RECEIVER_STATUS"} 2 | -------------------------------------------------------------------------------- /src/test/resources/status-chrome-mirroring-1.19.json: -------------------------------------------------------------------------------- 1 | {"requestId":1,"status":{"applications":[{"appId":"0F5096E8","displayName":"Chrome Mirroring","isIdleScreen":false,"namespaces":[{"name":"urn:x-cast:com.google.cast.webrtc"},{"name":"urn:x-cast:com.google.cast.media"},{"name":"urn:x-cast:com.google.cast.debug"}],"sessionId":"73D2358A-793C...","statusText":"null (Tab)","transportId":"shell-v2mirroring-1"}],"volume":{"controlType":"attenuation","level":1.0,"muted":false,"stepInterval":0.04}},"type":"RECEIVER_STATUS"} -------------------------------------------------------------------------------- /src/test/resources/status-chrome-mirroring-1.28.json: -------------------------------------------------------------------------------- 1 | {"requestId":1,"status":{"applications":[{"appId":"0F5096E8","displayName":"Chrome Mirroring","isIdleScreen":false,"launchedFromCloud":false,"namespaces":[{"name":"urn:x-cast:com.google.cast.webrtc"},{"name":"urn:x-cast:com.google.cast.media"},{"name":"urn:x-cast:com.google.cast.debug"},{"name":"urn:x-cast:com.google.cast.remoting"}],"sessionId":"...","statusText":" (Tab)","transportId":"..."}],"volume":{"controlType":"attenuation","level":1.0,"muted":false,"stepInterval":0.05000000074505806}},"type":"RECEIVER_STATUS"} 2 | -------------------------------------------------------------------------------- /src/test/resources/status-spotify.json: -------------------------------------------------------------------------------- 1 | {"requestId":1,"status":{"applications":[{"appId":"CC32E753","displayName":"Spotify","iconUrl":"https://lh3.googleusercontent.com/HOX9yqNu6y87Chb1lHYqhKVTQW43oFAFFe2ojx94yCLh0yMzgygTrM0RweAexApRWqq6UahgrWYimVgK","isIdleScreen":false,"launchedFromCloud":false,"namespaces":[{"name":"urn:x-cast:com.google.cast.debugoverlay"},{"name":"urn:x-cast:com.google.cast.cac"},{"name":"urn:x-cast:com.spotify.chromecast.secure.v1"},{"name":"urn:x-cast:com.google.cast.test"},{"name":"urn:x-cast:com.google.cast.broadcast"},{"name":"urn:x-cast:com.google.cast.media"}],"sessionId":"7fb71850-b38b-43bb-967e-e2c76b6d0990","statusText":"Spotify","transportId":"7fb71850-b38b-43bb-967e-e2c76b6d0990"}],"userEq":{"high_shelf":{"frequency":4500.0,"gain_db":0.0,"quality":0.707},"low_shelf":{"frequency":150.0,"gain_db":0.0,"quality":0.707},"max_peaking_eqs":0,"peaking_eqs":[]},"volume":{"controlType":"master","level":0.2258118838071823,"muted":false,"stepInterval":0.019999999552965164}},"type":"RECEIVER_STATUS"} -------------------------------------------------------------------------------- /src/test/resources/timetick.json: -------------------------------------------------------------------------------- 1 | {"requestId":0,"type":"TIME_TICK","timeElapsed":142,"timeRemaining":73,"track":"TR:2740060"} --------------------------------------------------------------------------------