Skip to content

Commit

Permalink
v1.0.0 - initial commit and public release
Browse files Browse the repository at this point in the history
  • Loading branch information
warren-bank committed Feb 25, 2020
0 parents commit 2bd894d
Show file tree
Hide file tree
Showing 42 changed files with 4,577 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Android generated
bin
gen
obj
lint.xml

# IntelliJ IDEA
.idea
*.iml
*.ipr
*.iws
classes
gen-external-apklibs

# Gradle
.gradle
build
buildout
out

# Other
.DS_Store
local.properties
339 changes: 339 additions & 0 deletions LICENSE.txt

Large diffs are not rendered by default.

191 changes: 191 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
#### [ExoPlayer AirPlay Receiver](https://github.com/warren-bank/Android-ExoPlayer-AirPlay-Receiver)
##### (less formally named: _"ExoAirPlayer"_)

Android app to run on a set-top box and play video URLs "cast" to it with a stateless HTTP API (based on AirPlay).

- - - -

#### Background:

* I use Chromecasts _a lot_
- they are incredibly adaptable
* though their protocol is [proprietary and locked down](https://blog.oakbits.com/google-cast-protocol-receiver-authentication.html)
- though the [Google Cast SDK for Android](https://developers.google.com/cast/docs/android_sender) is nearly ubiquitous, I very rarely cast video from apps
- I find much better video content to stream on websites, and wrote some tools to identify and cast these URLs
* [Chrome extension](https://github.com/warren-bank/crx-webcast-reloaded) to use with desktop web browsers
* [Android app](https://github.com/warren-bank/Android-WebCast) to use with mobile devices
* I also really like using Android set-top boxes
- mainly to play video files stored on an attached drive
- they are incredibly adaptable
* able to run any Android apk, such as:
- VPN client
- torrent client
- FTP client
- HTTP server
* I thought it would be "fun" to write an app to run on Android set-top boxes that could provide the same functionality that I enjoy on Chromecasts
- and will work equally well on smaller screens (ex: phones and tablets)

#### Scope:

* the goal is __not__ to provide an app that is recognized on the LAN as a virtual Chromecast device
- [CheapCast](https://github.com/mauimauer/cheapcast) accomplished this in 2013
* Google quickly [changed its protocol](https://blog.oakbits.com/google-cast-protocol-discovery-and-connection.html)
* AirPlay uses a very simple stateless [HTTP API](http://nto.github.io/AirPlay.html#video)
- this is a great starting point
* it supports: play, pause, seek, stop
- I'd like to extend this API (for a custom sender)
* to add support for: video queue, next, previous, mute, set volume

#### Design:

* [ExoPlayer](https://github.com/google/ExoPlayer)
- media player used to render video URLs
* [HttpCore](http://hc.apache.org/httpcomponents-core-ga/)
- low level HTTP transport components used to build a custom HTTP service
* [jmDNS](https://github.com/jmdns/jmdns)
- multi-cast DNS service registration used to make AirPlay-compatible HTTP service discoverable on LAN

- - - -

#### Usage (low level):

__AirPlay APIs:__

```bash
# network address for running instance of 'ExoPlayer AirPlay Receiver'
airplay_ip='192.168.1.100:8192'

# URLs for test videos:
videos_page='https://players.akamai.com/hls/'
video_url_1='https://multiplatform-f.akamaihd.net/i/multi/will/bunny/big_buck_bunny_,640x360_400,640x360_700,640x360_1000,950x540_1500,.f4v.csmil/master.m3u8'
video_url_2='https://multiplatform-f.akamaihd.net/i/multi/april11/hdworld/hdworld_,512x288_450_b,640x360_700_b,768x432_1000_b,1024x576_1400_m,.mp4.csmil/master.m3u8'
video_url_3='https://multiplatform-f.akamaihd.net/i/multi/april11/cctv/cctv_,512x288_450_b,640x360_700_b,768x432_1000_b,1024x576_1400_m,.mp4.csmil/master.m3u8'
```

* play video #1:
```bash
curl -X POST \
-H "Content-Type: text/parameters" \
--data-binary "Content-Location: ${video_url_1}\nStart-Position: 0" \
"http://${airplay_ip}/play"
```
* seek to `30 seconds` within currently playing video:
```bash
# note: POST body is required, even when it contains no data
curl -X POST \
--data-binary "" \
"http://${airplay_ip}/scrub?position=30.0"
```
* pause the currently playing video:
```bash
# note: POST body is required, even when it contains no data
curl -X POST \
--data-binary "" \
"http://${airplay_ip}/rate?value=0.0"
```
* resume playback of the currently paused video:
```bash
# note: POST body is required, even when it contains no data
curl -X POST \
--data-binary "" \
"http://${airplay_ip}/rate?value=1.0"
```
* increase speed of playback to 10x:
```bash
# note: POST body is required, even when it contains no data
curl -X POST \
--data-binary "" \
"http://${airplay_ip}/rate?value=10.0"
```
* stop playback:
```bash
# note: POST body is required, even when it contains no data
curl -X POST \
--data-binary "" \
"http://${airplay_ip}/stop"
```

__extended APIs:__

* add video #2 to end of queue (set 'Referer' request header, seek to 50%):
```bash
# note: position < 1 is a percent of the total video length
curl -X POST \
-H "Content-Type: text/parameters" \
--data-binary "Content-Location: ${video_url_2}\nReferer: ${videos_page}\nStart-Position: 0.5" \
"http://${airplay_ip}/queue"
```
* add video #3 to end of queue (set 'Referer' request header, seek to 30 seconds):
```bash
# note: position >= 1 is a fixed offset (in seconds)
curl -X POST \
-H "Content-Type: text/parameters" \
--data-binary "Content-Location: ${video_url_3}\nReferer: ${videos_page}\nStart-Position: 30" \
"http://${airplay_ip}/queue"
```
* skip forward to next video in queue:
```bash
# note: POST body is required, even when it contains no data
curl -X POST \
--data-binary "" \
"http://${airplay_ip}/next"
```
* skip backward to previous video in queue:
```bash
# note: POST body is required, even when it contains no data
curl -X POST \
--data-binary "" \
"http://${airplay_ip}/previous"
```
* mute audio:
```bash
# note: POST body is required, even when it contains no data
curl -X POST \
--data-binary "" \
"http://${airplay_ip}/volume?value=0.0"
```
* set audio volume to 50%:
```bash
# note: POST body is required, even when it contains no data
curl -X POST \
--data-binary "" \
"http://${airplay_ip}/volume?value=0.5"
```
* set audio volume to 100%:
```bash
# note: POST body is required, even when it contains no data
curl -X POST \
--data-binary "" \
"http://${airplay_ip}/volume?value=1.0"
```

#### Usage (high level):

__to do:__

* update [Chrome extension](https://github.com/warren-bank/crx-webcast-reloaded) to support "casting" videos from websites to a running instance of [ExoPlayer AirPlay Receiver](https://github.com/warren-bank/Android-ExoPlayer-AirPlay-Receiver)

- - - -

#### Credits:

* [AirPlay-Receiver-on-Android](https://github.com/gpfduoduo/AirPlay-Receiver-on-Android)
- __brilliant__
- what I like:
* quality of code is excellent
* implements 99% of what I've described
- [media player](https://github.com/yixia/VitamioBundle) used to render video URLs
- _HttpCore_ web server that implements all _AirPlay_ video APIs
- _jmDNS_ Bonjour registration
- what I dislike:
* all libraries are 5 years old
* doesn't use _ExoPlayer_
* the repo includes a lot of unused code
- needs a little housekeeping

- - - -

#### Legal:

* copyright: [Warren Bank](https://github.com/warren-bank)
* license: [GPL-2.0](https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt)
58 changes: 58 additions & 0 deletions android-studio-project/ExoPlayer-AirPlay-Receiver/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
apply from: '../constants.gradle'
apply plugin: 'com.android.application'

android {
compileSdkVersion project.ext.compileSdkVersion
buildToolsVersion project.ext.buildToolsVersion

compileOptions {
sourceCompatibility project.ext.javaVersion
targetCompatibility project.ext.javaVersion
}

defaultConfig {
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.targetSdkVersion

applicationId "com.github.warren_bank.exoplayer_airplay_receiver"
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
}

buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles = [
"proguard-rules.txt",
getDefaultProguardFile('proguard-android.txt')
]
}
debug {
debuggable true
jniDebuggable true
}
}

lintOptions {
disable 'MissingTranslation'
abortOnError true
}
}

dependencies {
compileOnly 'androidx.annotation:annotation:' + project.ext.libVersionAndroidX // ( 27 KB) https://mvnrepository.com/artifact/androidx.annotation/annotation
annotationProcessor 'androidx.annotation:annotation:' + project.ext.libVersionAndroidX

implementation 'com.google.android.exoplayer:exoplayer-core:' + project.ext.libVersionExoPlayer // (1.3 MB) https://mvnrepository.com/artifact/com.google.android.exoplayer/exoplayer-core?repo=bt-google-exoplayer
implementation 'com.google.android.exoplayer:exoplayer-dash:' + project.ext.libVersionExoPlayer // (107 KB) https://mvnrepository.com/artifact/com.google.android.exoplayer/exoplayer-dash?repo=bt-google-exoplayer
implementation 'com.google.android.exoplayer:exoplayer-hls:' + project.ext.libVersionExoPlayer // ( 98 KB) https://mvnrepository.com/artifact/com.google.android.exoplayer/exoplayer-hls?repo=bt-google-exoplayer
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:' + project.ext.libVersionExoPlayer // ( 44 KB) https://mvnrepository.com/artifact/com.google.android.exoplayer/exoplayer-smoothstreaming?repo=bt-google-exoplayer
implementation 'com.google.android.exoplayer:exoplayer-ui:' + project.ext.libVersionExoPlayer // (238 KB) https://mvnrepository.com/artifact/com.google.android.exoplayer/exoplayer-ui?repo=bt-google-exoplayer

implementation 'com.googlecode.plist:dd-plist:' + project.ext.libVersionDdPlist // ( 74 KB) https://mvnrepository.com/artifact/com.googlecode.plist/dd-plist
implementation 'org.apache.httpcomponents:httpcore:' + project.ext.libVersionHttpCore // (320 KB) https://mvnrepository.com/artifact/org.apache.httpcomponents/httpcore
implementation 'org.jmdns:jmdns:' + project.ext.libVersionJmDNS // (209 KB) https://mvnrepository.com/artifact/org.jmdns/jmdns
}

apply plugin: 'com.google.android.gms.strict-version-matcher-plugin'
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-keep class com.github.warren_bank.exoplayer_airplay_receiver.** { *; }

-keep class com.google.android.exoplayer.** { *; }
-keep class com.google.android.exoplayer2.** { *; }

-keep class javax.jmdns.** { *; }

-dontwarn javax.jmdns.test.**
-dontwarn org.apache.**
-dontwarn org.slf4j.**
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.github.warren_bank.exoplayer_airplay_receiver">

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

<application
android:name=".MainApp"
android:label="@string/app_name"
android:icon="@drawable/launcher"
android:largeHeap="true"
android:allowBackup="false"
android:supportsRtl="false">

<service
android:name=".service.NetworkingService"
android:enabled="true"
android:exported="true" />

<activity
android:name=".ui.StartNetworkingServiceActivity"
android:theme="@android:style/Theme.NoDisplay"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<activity
android:name=".ui.ImageActivity"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTask" />

<activity
android:name=".ui.VideoPlayerActivity"
android:theme="@android:style/Theme.NoTitleBar.Fullscreen"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:launchMode="singleTask" />

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.github.warren_bank.exoplayer_airplay_receiver;

import java.util.concurrent.ConcurrentHashMap;

import android.app.Application;
import android.os.Handler;
import android.os.Message;

public class MainApp extends Application {
private static MainApp instance;

private ConcurrentHashMap<String, Handler> mHandlerMap = new ConcurrentHashMap<String, Handler>();

public static MainApp getInstance() {
return instance;
}

public static void registerHandler(String name, Handler handler) {
getInstance().getHandlerMap().put(name, handler);
}

public static void unregisterHandler(String name) {
getInstance().getHandlerMap().remove(name);
}

public static void broadcastMessage(Message msg) {
for (Handler handler : getInstance().getHandlerMap().values()) {
handler.sendMessage(Message.obtain(msg));
}
}

@Override
public void onCreate() {
super.onCreate();
instance = this;
}

public ConcurrentHashMap<String, Handler> getHandlerMap() {
return mHandlerMap;
}
}
Loading

0 comments on commit 2bd894d

Please sign in to comment.