Skip to content

Latest commit

 

History

History
881 lines (821 loc) · 40.3 KB

File metadata and controls

881 lines (821 loc) · 40.3 KB
(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 v1).


Overview:

There is no UI when the app starts. It's a foreground service with a notification, which runs a web server on port 8192. The IP address of the server is given in the notification message.

When a video URL is "cast" to the server, a video player opens full-screen.

When an audio URL is "cast" to the server, the music plays in the background.. even when the screen is off.

When either audio or video media is playing and the player's window doesn't have focus (ex: listening to background audio, or by pressing the "home" button while watching a video), another notification is added to control playback or refocus the player's window.

This page is the simplest way to send signals to a running instance, though other "high level" tools exist to capture media URLs from the wild.

Audio or video files/playlists can also be started directly from the Android file system, which makes this app a very suitable replacement for a general-purpose video player.

Playlists can be generated dynamically from:

  • a single directory in the Android file system
  • a recursive directory tree in the Android file system
  • any HTML page with anchor tags that link to media files
    • very useful for playing files from a remote directory listing

Playlists can be read explicitly from any text file with an .m3u file extension, which lists one media item path per line:

  • the .m3u file can be read from either the Android file system or a remote URL
  • each item path can refer to either the Android file system or a remote URL

When a video file is played from the Android file system, its directory is automatically scanned for matching subtitle file(s). A match will have the same filename and any of the following extensions: srt,ttml,vtt,webvtt,ssa,ass. Nested extension(s) can optionally be used to distinguish between different languages (ex: .en-US.srt, .es-MX.vtt).


Background:

  • I use Chromecasts a lot
  • 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
  • AirPlay v1 uses a very simple stateless HTTP API
    • 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
        • audio playlists (m3u, html directory index)

Design:

  • ExoPlayer
    • media player used to render video URLs
  • HttpCore
    • low level HTTP transport components used to build a custom HTTP service
  • jmDNS
    • multi-cast DNS service registration used to make the AirPlay v1 compatible HTTP service discoverable on LAN

Usage (low level):

AirPlay v1 compatible APIs:

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

  # file path for test image (on sender):
  image_path='/path/to/image.jpg'

  # URL for test image:
  image_page='https://commons.wikimedia.org/wiki/File:Android_robot.svg'
  image_url='https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Android_robot.svg/654px-Android_robot.svg.png'

  # URLs for test video:
  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'

  # URLs for test video text captions:
  captions_page='https://github.com/gpac/gpac/tree/master/tests/media/webvtt'
  caption_url_1='https://github.com/warren-bank/Android-ExoPlayer-AirPlay-Receiver/raw/v02/tests/.captions/counter.workaround-exoplayer-issue-7122.srt'
  caption_url_2='https://github.com/warren-bank/Android-ExoPlayer-AirPlay-Receiver/raw/v02/tests/.captions/counter.vtt'
  caption_url_3='https://github.com/gpac/gpac/raw/master/tests/media/webvtt/comments.vtt'

  # URLs for test video DRM:
  #   https://exoplayer.dev/drm.html
  #     widevine:  requires Android 4.4+
  #     clearkey:  requires Android 5.0+
  #     playready: requires AndroidTV
  drm_videos_page='https://github.com/google/ExoPlayer/blob/r2.14.0/demos/main/src/main/assets/media.exolist.json'
  drm_video_url_1='https://storage.googleapis.com/wvmedia/cenc/h264/tears/tears.mpd'
  drm_video_url_1_license_scheme='widevine'
  drm_video_url_1_license_server='https://proxy.uat.widevine.com/proxy?provider=widevine_test'
  drm_video_url_2='https://playready.directtaps.net/smoothstreaming/SSWSS720H264PR/SuperSpeedway_720.ism/Manifest'
  drm_video_url_2_license_scheme='playready'
  drm_video_url_2_license_server='https://playready.directtaps.net/pr/svc/rightsmanager.asmx'

  # URLs for test audio:
  audio_flac_nfo='https://archive.org/details/tntvillage_457399'
  audio_flac_url='https://archive.org/download/tntvillage_457399/Black%20Sabbath%201970-2013/Studio%20Albums/1970%20Black%20Sabbath/1970%20Black%20Sabbath%20%5B1986%20France%20NELCD%206002%20Castle%5D/Black%20Sabbath%20-%20Black%20Sabbath%20%281986%2C%20Castle%20Communications%2C%20NELCD%206002%29.flac'
  audio_m3u_page='https://archive.org/details/Mozart_Vesperae_Solennes_de_Confessore'
  audio_mp3s_m3u='https://archive.org/download/Mozart_Vesperae_Solennes_de_Confessore/Mozart%20-%20Vesper%C3%A6%20Solennes%20de%20Confessore%20%28Cooke%29.m3u'
  audio_htm_page='https://archive.org/details/tntvillage_455310'
  audio_mp3s_htm='https://archive.org/download/tntvillage_455310/S%26G/Live/1967%20-%20Live%20From%20New%20York%20City%20%40320/'

  # file paths for test media (on receiver):
  video_path='/storage/external_SD/test-media/video/file.mp4'
  subtt_path='/storage/external_SD/test-media/video/file.srt'
  audio_path='/storage/external_SD/test-media/audio/file.mp3'
  plist_path='/storage/external_SD/test-media/all audio and video files.m3u'

  # directory paths for test media (on receiver):
  video_dir_path='/storage/external_SD/test-media/video/'
  audio_dir_path='/storage/external_SD/test-media/audio/'
  recursive_path='/storage/external_SD/test-media/'
  • display image from local file system (on sender):
      curl --silent -X POST \
        --data-binary "@${image_path}" \
        "http://${airplay_ip}/photo"
  • display image from remote URL:
      curl --silent "$image_url" | \
      curl --silent -X POST \
        --data-binary @- \
        "http://${airplay_ip}/photo"
  • play video #1 (seek to beginning):
      curl --silent -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:
      curl --silent -X GET \
        "http://${airplay_ip}/scrub?position=30.0"
  • pause the currently playing video:
      curl --silent -X GET \
        "http://${airplay_ip}/rate?value=0.0"
  • resume playback of the currently paused video:
      curl --silent -X GET \
        "http://${airplay_ip}/rate?value=1.0"
  • increase speed of playback to 10x:
      curl --silent -X GET \
        "http://${airplay_ip}/rate?value=10.0"
  • stop playback:
      curl --silent -X GET \
        "http://${airplay_ip}/stop"

extended APIs:

  • seek 30 seconds forward relative to current position within currently playing video (30 second = 30*1e3 milliseconds):
      curl --silent -X GET \
        "http://${airplay_ip}/add-scrub-offset?value=30000"
  • seek 30 seconds backward relative to current position within currently playing video (30 second = 30*1e3 milliseconds):
      curl --silent -X GET \
        "http://${airplay_ip}/add-scrub-offset?value=-30000"
  • play video #1 (add text captions, set 'Referer' request header, seek to beginning):
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${video_url_1}\nCaption-Location: ${caption_url_1}\nReferer: ${videos_page}\nStart-Position: 0" \
        "http://${airplay_ip}/play"
  • add video #2 to end of queue (add text captions, set 'Referer' request header, seek to 50%):
      # note: position < 1 is a percent of the total track length
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${video_url_2}\nCaption-Location: ${caption_url_2}\nReferer: ${videos_page}\nStart-Position: 0.5" \
        "http://${airplay_ip}/queue"
  • add video #3 to end of queue (add text captions, set 'Referer' request header, seek to 30 seconds):
      # note: position >= 1 is a fixed offset (in seconds)
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${video_url_3}\nCaption-Location: ${caption_url_3}\nReferer: ${videos_page}\nStart-Position: 30" \
        "http://${airplay_ip}/queue"
  • play video #1 and add videos #2 and #3 to end of queue (set 'Referer' request header):
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${video_url_1}\nContent-Location: ${video_url_2}\nContent-Location: ${video_url_3}\nReferer: ${videos_page}" \
        "http://${airplay_ip}/play"
  • play DRM video #1 (seek to 10 seconds, end playback at 30 seconds):
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${drm_video_url_1}\nDRM-License-Scheme: ${drm_video_url_1_license_scheme}\nDRM-License-Server: ${drm_video_url_1_license_server}\nStart-Position: 10\nStop-Position: 30" \
        "http://${airplay_ip}/play"
  • add DRM video #2 to end of queue (seek to 50%):
      # note: position < 1 is a percent of the total track length
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${drm_video_url_2}\nDRM-License-Scheme: ${drm_video_url_2_license_scheme}\nDRM-License-Server: ${drm_video_url_2_license_server}\nStart-Position: 0.5" \
        "http://${airplay_ip}/queue"
  • skip forward to next video in queue:
      curl --silent -X GET \
        "http://${airplay_ip}/next"
  • skip backward to previous video in queue:
      curl --silent -X GET \
        "http://${airplay_ip}/previous"
  • mute audio:
      curl --silent -X GET \
        "http://${airplay_ip}/volume?value=0.0"
  • set audio volume to 50%:
      curl --silent -X GET \
        "http://${airplay_ip}/volume?value=0.5"
  • set audio volume to 100%:
      curl --silent -X GET \
        "http://${airplay_ip}/volume?value=1.0"
  • set audio volume to 100% and amplify by 10.5 dB:
      # note: audio amplification requires Android 4.4+
      curl --silent -X GET \
        "http://${airplay_ip}/volume?value=11.5"
  • load new text captions for current video in queue:
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Caption-Location: ${caption_url_1}" \
        "http://${airplay_ip}/load-captions"
  • turn text captions off:
      curl --silent -X GET \
        "http://${airplay_ip}/show-captions?toggle=0"
  • turn text captions on:
      curl --silent -X GET \
        "http://${airplay_ip}/show-captions?toggle=1"
  • set font style and size options for text captions to custom values:
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Apply-Embedded: false\nFont-Size: 20" \
        "http://${airplay_ip}/set-captions-style"
  • set font style and size options for text captions to default values:
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Apply-Embedded: true\nFont-Size: 0" \
        "http://${airplay_ip}/set-captions-style"
  • set time offset for text captions (1 second = 1e6 microseconds):
      curl --silent -X GET \
        "http://${airplay_ip}/set-captions-offset?value=1000000"
  • add to current time offset for text captions (60 second = 60*1e6 microseconds):
      curl --silent -X GET \
        "http://${airplay_ip}/add-captions-offset?value=60000000"
  • remove time offset for text captions:
      curl --silent -X GET \
        "http://${airplay_ip}/set-captions-offset?value=0"
  • set repeat mode:
      # note: supported values: [off,one,all]. default: all
      curl --silent -X GET \
        "http://${airplay_ip}/repeat-mode?value=all"
  • set resize mode:
      # note: supported values: [fit,width,height,fill,zoom]. default: fit
      curl --silent -X GET \
        "http://${airplay_ip}/resize-mode?value=fit"
  • play audio .flac file (set 'Referer' request header, seek to 50%):
      # note: position < 1 is a percent of the total track length
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${audio_flac_url}\nReferer: ${audio_flac_nfo}\nStart-Position: 0.5" \
        "http://${airplay_ip}/play"
  • play audio .m3u playlist (6 songs, set 'Referer' request header for all songs, seek to 30 seconds in first song):
      # note: position >= 1 is a fixed offset (in seconds)
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${audio_mp3s_m3u}\nReferer: ${audio_m3u_page}\nStart-Position: 30" \
        "http://${airplay_ip}/play"
  • add audio .html directory index playlist to end of queue (20 songs, set 'Referer' request header for all songs, seek to beginning of first song):
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${audio_mp3s_htm}\nReferer: ${audio_htm_page}\nStart-Position: 0" \
        "http://${airplay_ip}/queue"
  • play video from file system on receiver (add text captions, seek to beginning):
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${video_path}\nCaption-Location: ${subtt_path}\nStart-Position: 0" \
        "http://${airplay_ip}/play"
  • add audio from file system on receiver to end of queue (seek to 50%):
      # note: position < 1 is a percent of the total track length
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${audio_path}\nStart-Position: 0.5" \
        "http://${airplay_ip}/queue"
  • play combination of audio and video files in order specified by .m3u playlist from file system on receiver:
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${plist_path}" \
        "http://${airplay_ip}/play"
  • play all audio and video files in specified directory from file system on receiver:
      # note: IF the specified directory contains one or more media files, THEN does not recursively search for media files in subdirectories
      #       IF the specified directory does not contain any media files, THEN does recursively search for media files in all subdirectories
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${video_dir_path}" \
        "http://${airplay_ip}/play"
  • queue all audio and video files in specified directory from file system on receiver:
      # note: IF the specified directory contains one or more media files, THEN does not recursively search for media files in subdirectories
      #       IF the specified directory does not contain any media files, THEN does recursively search for media files in all subdirectories
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${audio_dir_path}" \
        "http://${airplay_ip}/queue"
  • play all audio and video files by recursively searching within specified directory from file system on receiver:
      # note: IF the specified directory contains one or more media files, THEN does not recursively search for media files in subdirectories
      #       IF the specified directory does not contain any media files, THEN does recursively search for media files in all subdirectories
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "Content-Location: ${recursive_path}" \
        "http://${airplay_ip}/play"
  • show the video player in the top-most foreground Activity:
      curl --silent -X GET \
        "http://${airplay_ip}/show-player"
  • show the video player in picture-in-picture (PiP) mode:
      # note: PiP mode is only available on Android TV 7.0 or Android 8.0 and higher
      curl --silent -X GET \
        "http://${airplay_ip}/show-player-pip"
  • hide the video player so it is neither the top-most foreground Activity nor in PiP mode:
      # note: audio playback will continue in the background
      curl --silent -X GET \
        "http://${airplay_ip}/hide-player"
  • show a Toast containing a custom message:
      curl --silent -X POST \
        -H "Content-Type: text/plain" \
        --data-binary "Lorem Ipsum" \
        "http://${airplay_ip}/show-toast"
  • start an Activity with custom Intent attributes:
      post_body='
        package:
        class:
        action: android.intent.action.VIEW
        data: http://example.com/video.m3u8
        type: application/x-mpegurl
        category: android.intent.category.DEFAULT
        category: android.intent.category.BROWSABLE
        flag: 0x10000000
        flag: 0x00008000
        extra-referUrl: http://example.com/videos.html
        extra-textUrl: http://example.com/video.srt
        extra-useCache: true
        extra-startPos:
        extra-stopPos:
        extra-drmScheme: widevine
        extra-drmUrl: http://widevine.example.com/
    
        extra-reqHeader: Referer: http://example.com/videos.html
        extra-reqHeader: Origin: http://example.com
        extra-reqHeader: X-Requested-With: XMLHttpRequest
        extra-reqHeader: User-Agent: Chrome/90
    
        extra-drmHeader: Authorization: Bearer xxxxx
        extra-drmHeader: Cookie: token=xxxxx; sessionID=yyyyy
    
        chooser-title: Open HLS video stream in:
      '
    
      curl --silent -X POST \
        -H "Content-Type: text/parameters" \
        --data-binary "${post_body}" \
        "http://${airplay_ip}/start-activity"
  • exit the Service:
      # note: also closes the video player foreground Activity, and kills the process
      curl --silent -X GET \
        "http://${airplay_ip}/exit-service"

Notes:

  • POST data sent in requests to /play and /queue API endpoints:
    • contains one key:value pair per line of text
    • lines of text containing unrecognized keys are ignored
    • keys and values can be separated by either : or = characters, with optional whitespace
    • keys are not case sensitive
    • recognized keys include:
      • content-location
        • use key on multiple lines to declare more than one value
      • caption-location
      • referer
      • req-header
        • use key on multiple lines to declare more than one value
      • use-cache
      • start-position
      • stop-position
      • drm-license-scheme
        • valid values include:
          • widevine
          • clearkey
          • playready
      • drm-license-server
      • drm-header
        • use key on multiple lines to declare more than one value
    • keys required:
      • content-location
  • POST data sent in requests to /load-captions API endpoint:
    • contains one key:value pair per line of text
    • lines of text containing unrecognized keys are ignored
    • keys and values can be separated by either : or = characters, with optional whitespace
    • keys are not case sensitive
    • recognized keys include:
      • caption-location
  • POST data sent in requests to /set-captions-style API endpoint:
    • contains one key:value pair per line of text
    • lines of text containing unrecognized keys are ignored
    • keys and values can be separated by either : or = characters, with optional whitespace
    • keys are not case sensitive
    • recognized keys include:
      • apply-embedded
        • apply styles and sizes embedded in the text captions?
      • font-size
        • unit: sp
        • note: value 0 is special and used to revert to default
  • POST data sent in requests to /start-activity API endpoint:
    • contains one key:value pair per line of text
    • lines of text containing unrecognized keys are ignored
    • lines of text containing no value are ignored
    • keys and values can be separated by either : or = characters, with optional whitespace
    • keys are case sensitive
    • recognized keys include:
      • package
      • class
      • action
      • data
        • URI
      • type
        • content/mime type
        • value is not normalized to lower-case
      • category
        • use key on multiple lines to declare more than one value
      • flag
        • use key on multiple lines to declare more than one value
        • format value in decimal (base 10), or hex (base 16) with "0x" prefix
      • extra-*
        • where "*" is a glob pattern
          • to indicate that the recognized key can contain any additional sequence of characters
          • to capture the case sensitive name of an Intent extra
        • the data type of the Intent extra is determined as follows:
          • if a distinct key occurs on multiple lines
            • String[]
          • if the value begins with an explicit type cast
            • matching any of the following case insensitive substrings:
              • (String)
              • (String[])
              • (bool)
              • (bool[])
              • (boolean)
              • (boolean[])
              • (byte)
              • (byte[])
              • (char)
              • (char[])
              • (double)
              • (double[])
              • (float)
              • (float[])
              • (int)
              • (int[])
              • (integer)
              • (integer[])
              • (long)
              • (long[])
              • (short)
              • (short[])
            • notes:
              • when a value is explicitly cast to an array..
                • if the data type is: String[]
                  • the value is not tokenized
                  • the array:
                    • has length: 1
                    • is equal to: [value]
                • if the data type is: char[]
                  • the value is parsed as an ordered sequence of characters
                  • commas are removed
                • for all other data types
                  • the value is parsed as an ordered comma-separated list
          • if the value can be implicitly cast
            (note: the following regex patterns are only descriptive)
            • matches the regex pattern: /(true|false)/i
              • boolean
            • matches the regex pattern: /[+-]?\d+/
              • int
            • matches the regex pattern: /[+-]?\d+L/i
              • long
            • matches the regex pattern: /[+-]?\d+(\.\d+)?F/i
              • float
            • matches the regex pattern: /[+-]?\d+(\.\d+)?D/i
              • double
          • otherwise
            • String
      • chooser-title
        • a non-empty value indicates that a chooser dialog should always be shown
        • the value is the title to display in the chooser dialog
    • keys required to start an explicit Intent:
      • package and class
    • keys required to start an implicit Intent:
      • action
    • all other keys are optional
  • POST data sent in requests to /share-video API endpoint:
    • contains one key:value pair per line of text
    • lines of text containing unrecognized keys are ignored
    • keys and values can be separated by either : or = characters, with optional whitespace
    • keys are not case sensitive
    • recognized keys include:
      • referUrl
      • reqHeader
      • textUrl
      • drmScheme
      • drmUrl
      • drmHeader
    • the Intent to start a new Activity with values derived from the current video in the queue includes..
      • data URI and type
      • extras:
        • (String) referUrl
        • (String) textUrl
        • (String) drmScheme
        • (String) drmUrl
        • (String[]) reqHeader
        • (String[]) drmHeader
    • POST data can map extras from the default names (above) to alias names
    • extras having String[] values..
      • format each String in the Array as "name: value"
      • since this is a non-standard format, when these extras are duplicated to an alias name, the format is converted to a Bundle w/ String based key-value pairs
    • POST data example:
        referUrl:  Referer
        reqHeader: android.media.intent.extra.HTTP_HEADERS
      
  • POST data sent in requests to /edit-preferences API endpoint:
    • contains one key:value pair per line of text
    • lines of text containing unrecognized keys are ignored
    • keys and values can be separated by either : or = characters, with optional whitespace
    • keys are not case sensitive
    • recognized keys include:
      • default-user-agent
        • type: string
        • description: default User-Agent HTTP request header
        • default: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4710.39 Safari/537.36
      • max-audio-volume-boost-db
        • type: integer
        • description: maximum number of dB that the audio can be amplified
        • default: 50
      • max-parallel-downloads
        • type: integer
        • description: maximum number of threads used to download each video in parallel
        • default: 6
        • limitation: update does not take effect until the app is restarted
      • seek-back-ms-increment
        • type: integer
        • description: number of milliseconds that the rewind icon in the video controls overlay will rewind within the current video
        • default: 5000 (ie: 5 seconds)
        • limitation: update does not take effect until the app is restarted
      • seek-forward-ms-increment
        • type: integer
        • description: number of milliseconds that the fast-forward icon in the video controls overlay will fast-forward within the current video
        • default: 15000 (ie: 15 seconds)
        • limitation: update does not take effect until the app is restarted
      • audio-volume-percent-increment
        • type: float
        • description: percent of full volume that is changed each time a hardware volume button is pressed
        • context: applies when volume is < 100%
        • default: 0.05 (ie: 5%)
      • audio-volume-boost-db-increment
        • type: float
        • description: number of dB that volume amplification is changed each time a hardware volume button is pressed
        • context: applies when volume is > 100%
        • default: 0.50
      • ts-extractor-timestamp-search-bytes-factor
        • type: float
        • description: multiplication factor used to adjust the maximum number of bytes to search at the start/end of each media file to obtain its first and last Program Clock Reference (PCR) values to compute the duration
        • default: 2.50
      • enable-tunneled-video-playback
        • type: boolean
        • description: enable tunneled video playback?
        • default: false
      • enable-hdmv-dts-audio-streams
        • type: boolean
        • description: enable the handling of HDMV DTS audio streams?
        • default: false
      • pause-on-change-to-audio-output-device
        • type: boolean
        • description: pause automatically when audio is rerouted from a headset to device speakers?
        • default: false
      • prefer-extension-renderer
        • type: boolean
        • description: prefer to use an extension renderer to a core renderer?
        • default: false
        • limitation: update does not take effect until the app is restarted
  • POST data sent in requests to /show-toast API endpoint:
    • contains an arbitrary block of text

Usage (high level):

  • single-page application (SPA) that can run in any web browser, and be used to:

    • send commands to a running instance of ExoPlayer AirPlay Receiver
      • "cast" video URLs to its playlist
      • control all aspects of playback
  • WebCast-Reloaded Chrome extension that can run in any Chromium-based desktop web browser, and be used to:

    • intercept the URL of (nearly) all videos on any website
    • display these video URLs as a list of links
      • clicking on any link will transfer the URL of the video (as well as the URL of the referer webpage) to the SPA (above)
        • more precisely, the link to the SPA is displayed as a small AirPlay icon AirPlay icon
        • the other links transfer the video URL to other tools
          • webpage to watch the video in an HTML5 player with the ability to "cast" the video to a Chromecast
          • a running instance of HLS-Proxy
  • WebCast Android app that is open-source, and can be used to:

    • intercept the URL of (nearly) all videos on any website
    • display these video URLs as a list
    • when the app's settings are configured to use an external video player:
      • clicking on any video will broadcast an Intent to start the video in another application (ex: ExoAirPlayer)
  • Greasemonkey userscripts

    • that can run in any web browser with support for userscripts:
    • and be used to:
      • apply site-specific knowledge to obtain the URL of a video on the requested page
      • in WebMonkey:
        • broadcast an Intent to start the video in another application (ex: ExoAirPlayer)
      • in other web browsers:
        • automatically redirect to the SPA (above)
  • Toaster Cast Android app that can be used to:

    • discover AirPlay v1 receivers on the same LAN
    • discover DLNA media servers on the same LAN
      • optionally, runs a local DLNA media server
      • browse media on all servers
      • casts the URL of media hosted by a DLNA server to the receiver
  • WebTorrent Desktop app (for Windows, Mac, Linux) that is open-source, and can be used to:

    • download videos from a p2p network
      • supports connections to peers using both BitTorrent (TCP) and WebTorrent (WebRTC)
    • discover AirPlay v1 receivers on the same LAN
    • stream videos that are fully or partially downloaded
      • runs a local web server
      • casts the URL of the video to the receiver
      • serves the video file when requested by the receiver
  • DroidPlay Android app that is open-source, and can be used to:

    • discover AirPlay v1 receivers on the same LAN
    • display images from the local file system
      • sends the entire file in POST data to the receiver
    • stream videos from the local file system
      • runs a local web server
      • casts the URL of the video to the receiver
      • serves the video file when requested by the receiver
  • fork of: DroidPlay Android app that is open-source, and can additionally be used to:

    • mirror the screen to an AirPlay v1 receiver
      • only available on Android 5.0 and higher
  • airplay desktop app (for Java JRE) that is open-source, and can be used to:

    • mirror the screen to an AirPlay v1 receiver
        airplay_ip='192.168.1.100:8192'
        java -jar "airplay.jar" -h "$airplay_ip" -d
  • exoairtube desktop app (for Node.js) that is open-source, and can be used to:

    • discover AirPlay v1 receivers on the same LAN
    • stream videos hosted by YouTube
      • supports YouTube playlists
      • casts the URL of the highest quality video format available to the receiver

Other Tools (high level):

  • HTTP Shortcuts Android app that is open-source, and can be used to:

  • Bookmarks Android app that is open-source, and can be used to:

    • bookmark media streams
      • configure Intents that play media streams in ExoAirPlayer… running on the same device
        • example:
          • Name = CBS News
          • Action = android.intent.action.VIEW
          • Package Name = com.github.warren_bank.exoplayer_airplay_receiver
          • Class Name = com.github.warren_bank.exoplayer_airplay_receiver.ui.StartNetworkingServiceActivity
          • Data URI = https://www.cbsnews.com/common/video/cbsn_header_prod.m3u8
          • Data (MIME) Type = application/x-mpegurl
          • extra:
            • Name = referUrl
            • Type of Value = String
            • Value = https://www.cbsnews.com/live/
        • notes:
          • this example configures an explicit Intent
            • the media stream will always play in ExoAirPlayer… running on the same device
          • an implicit Intent could also be configured
            • the following configuration fields can be empty:
              • Package Name
              • Class Name
            • as a result, an Activity chooser dialog will prompt the user to select from a list of all installed apps that match the implicit Intent… which includes ExoAirPlayer
          • the HLS video stream for CBS News doesn't actually require a Referer HTTP request header, but one is added to illustrate how to do so
      • start an Activity for any such bookmarked Intent by either:
        • triggering manually (method 1)
          • click on the chosen Intent
        • triggering manually (method 2)
          • long click on the chosen Intent to open its context menu
          • select: Perform… > Start Activity
        • scheduling an alarm
          • long click on the chosen Intent to open its context menu
          • select: Schedule…
            • Perform = Start Activity
            • configure other settings: date, time, interval, precision, etc…
    • simulate keypress of media keys
      • configure Intents that broadcast media key events to ExoAirPlayer… running on the same device
        • example:
          • Name = stop
          • Action = android.intent.action.MEDIA_BUTTON
          • Package Name = com.github.warren_bank.exoplayer_airplay_receiver
          • Class Name = androidx.media.session.MediaButtonReceiver
          • extra:
            • Name = android.intent.extra.KEY_EVENT
            • Type of Value = int
            • Value = 86
        • notes:
          • this example configures an explicit Intent
          • Android 8.0+ does not allow any app to receive implicit broadcasts for media key events
          • if ExoAirPlayer is used on a device that is running a version of Android < 8.0, then an implicit Intent is sufficient
            • the following configuration fields can be empty:
              • Package Name
              • Class Name
            • as a result, broadcast receivers in all installed apps that match the implicit Intent will be notified
      • broadcast any such bookmarked Intent by either:
        • triggering manually
          • long click on the chosen Intent to open its context menu
          • select: Perform… > Send Broadcast
        • scheduling an alarm
          • long click on the chosen Intent to open its context menu
          • select: Schedule…
            • Perform = Send Broadcast
            • configure other settings: date, time, interval, precision, etc…

Credits:

  • AirPlay-Receiver-on-Android
    • brilliant
    • what I like:
      • quality of code is excellent
      • implements much of the foundation for what I've described
        • media player used to render video URLs
        • HttpCore web server that implements all AirPlay video APIs
        • jmDNS Bonjour registration
    • what I dislike:
      • all libraries are several years old
      • doesn't use ExoPlayer
      • the repo includes a lot of unused code
        • needs a little housekeeping

Legal: