_Node.js HLS Proxy_ : HTTP Live Streaming Proxy for the _Node JS_ Runtime
Basic Functionality:
- proxy .m3u8 files, and the video segments (.ts files) they internally reference
- to all proxied files:
- to .m3u8:
Advanced Features:
- inject custom HTTP headers in all outbound proxied requests
- prefetch video segments (.ts files)
- use a hook function to conditionally decide which video segments to prefetch
- use a hook function to conditionally redirect URLs in the playlist (_before_ and/or _after_ they're modified to pass through the proxy)
- use a hook function to conditionally rewrite URLs after they're received by the proxy
Benefits:
- any video player (on the LAN) can access the proxied video stream
- prefetch and caching of video segments ahead-of-time makes playback of the video stream very stable
- the proxy can easily be configured to bypass many of the security measures used by video servers to restrict access:
Origin and Referer are often inspected by the server
* when these headers don't match the site hosting the content, a 403 Forbidden response is returned (in lieu of the requested data)
* restricted access to encryption keys
* often times the encrypted video segments (.ts files) are readily available, but the encryption keys are well protected
* if the keys can be obtained from another source, then a hook function can be used to redirect only those URL requests
- - - -
URL Format:
- [example Javascript]: construction of URL to _HLS Proxy_ for video stream
{
const proxy_url = 'http://127.0.0.1:8080'
const video_url = 'https://example.com/video/master.m3u8'
const file_extension = '.m3u8'
const hls_proxy_url = ${proxy_url}/${ btoa(video_url) }${file_extension}
}
- [example Javascript]: construction of URL to _HLS Proxy_ for video stream w/ "Referer" request header
{
const proxy_url = 'http://127.0.0.1:8080'
const video_url = 'https://example.com/video/master.m3u8'
const referer_url = 'https://example.com/videos.html'
const file_extension = '.m3u8'
const hls_proxy_url = ${proxy_url}/${ btoa(${video_url}|${referer_url}) }${file_extension}
}
- [example Javascript]: construction of URL to _HLS Proxy_ for video stream w/ "Referer" and additional request headers
{
const proxy_url = 'http://127.0.0.1:8080'
const video_url = 'https://example.com/video/master.m3u8'
const referer_url = 'https://example.com/videos.html'
const file_extension = '.m3u8'
const headers = {"Cookie": "foo=bar"}
const querystring = ?headers=${ encodeURIComponent( btoa( JSON.stringify(headers) ) ) }
const hls_proxy_url = ${proxy_url}/${ btoa(${video_url}|${referer_url}) }${file_extension}${querystring}
}
- [example Bash]: construction of URL to _HLS Proxy_ for video stream
proxy_url='http://127.0.0.1:8080'
video_url='https://example.com/video/master.m3u8'
file_extension='.m3u8'
hls_proxy_url="${proxy_url}/"$(echo -n "$video_url" | base64 --wrap=0)"$file_extension"
- [example Bash]: construction of URL to _HLS Proxy_ for video stream w/ "Referer" request header
proxy_url='http://127.0.0.1:8080'
video_url='https://example.com/video/master.m3u8'
referer_url='https://example.com/videos.html'
file_extension='.m3u8'
hls_proxy_url="${proxy_url}/"$(echo -n "${video_url}|${referer_url}" | base64 --wrap=0)"$file_extension"
- [example Bash]: construction of URL to _HLS Proxy_ for video stream w/ "Referer" and additional request headers
proxy_url='http://127.0.0.1:8080'
video_url='https://example.com/video/master.m3u8'
referer_url='https://example.com/videos.html'
file_extension='.m3u8'
headers_json='{"Cookie": "foo=bar"}'
querystring='?headers='$(echo -n "$headers_json" | base64 --wrap=0)
hls_proxy_url="${proxy_url}/"$(echo -n "${video_url}|${referer_url}" | base64 --wrap=0)"${file_extension}${querystring}"
##### notes:
- adding a file extension to the base64 encoded video URL is highly recommended
.m3u8HLS manifest *
.tsmedia segment *
.keyencryption key *
.jsonJSON data - though currently, *
.m3u8is the only file extension that receives special treatment * all other file types (including those without any file extension) are piped directly to the HTTP response
##### high-level tools that automate this task:
- refer to the section: Other Projects
- - - -
Installation and Usage: Globally
How to: Install:
npm install --global "@warren-bank/hls-proxy"
How to: Run the server(s):
hlsd <options>
options:
========
--help
--version
--tls
--host <host>
--port <number>
--copy-req-headers
--req-headers <filepath>
--origin <header>
--referer <header>
--useragent <header>
--header <name=value>
--req-options <filepath>
--req-insecure
--req-secure-honor-server-cipher-order
--req-secure-ciphers <string>
--req-secure-protocol <string>
--req-secure-curve <string>
--hooks <filepath>
--prefetch
--max-segments <number>
--cache-timeout <number>
--cache-key <number>
--cache-storage <adapter>
--cache-storage-fs-dirpath <dirpath>
-v <number>
--acl-ip <ip_address_list>
--acl-pass <password_list>
--block-req-hostname <hostname_list>
--http-proxy <http[s]://[user:pass@]hostname:port>
--tls-cert <filepath>
--tls-key <filepath>
--tls-pass <filepath>
--manifest-extension <ext>
--segment-extension <ext>
Options:
- _--tls_ is a flag to start HTTPS proxy, rather than HTTP
./hls-proxy/servers/cert
* when all of these option are properly specified:
* the https: protocol is used by all URLs in modified HLS manifests
- _--host_ is an IP or hostname with an optional port number that can be resolved and is reachable by clients
192.168.0.100:8080
* used to modify URLs in .m3u8 files
* when this option is specified without a port number:
* the value of the _--port_ option is appended
* when this option is specified and the port number is 443:
* the https: protocol is used by all URLs in modified HLS manifests
* when this option is not specified:
* the value of the "Host" HTTP request header is used
- _--port_ is the port number that the server listens on
8080
* when this option is not specified:
* HTTP proxy binds to: 80
* HTTPS proxy binds to: 443
- _--copy-req-headers_ is a flag to enable the duplication of all HTTP request headers sent to the proxy → to the request made by the proxy to the video server
- _--req-headers_ is the filepath to a JSON data _Object_ containing key:value pairs
- _--origin_ is the value of the corresponding HTTP request header
- _--referer_ is the value of the corresponding HTTP request header
- _--useragent_ is the value of the corresponding HTTP request header
- _--header_ is a single name:value pair
- _--req-options_ is the filepath to a JSON data _Object_
http.request(options)
* https.request(options)
* advanced __https__ request options:
* context of the secure request is obtained by passing the request options _Object_ to: tls.createSecureContext(options)
* configuration for the context of the secure request can be merged with the request options _Object_
* configuration keys of particular interest:
* honorCipherOrder
* default value: false
* ciphers
* default value: "ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:DHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA256:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!SRP:!CAMELLIA"
* secureProtocol
* default value: "TLS_method"
* ecdhCurve
* default value: tls.DEFAULT_ECDH_CURVE
* the exact value depends on the version of node
* most commonly:
* older versions of node: "prime256v1"
* newer versions of node: "auto"
- _--req-insecure_ is a flag to override the following environment variable to disable certificate validation for secure __https__ requests:
NODE_TLS_REJECT_UNAUTHORIZED= 0
* equivalent to:
* curl --insecure
* wget --no-check-certificate
- _--req-secure-honor-server-cipher-order_ is a flag to set the following key in the request options _Object_ to configure the context for secure __https__ requests:
{honorCipherOrder: true}
- _--req-secure-ciphers_ is the value to assign to the following key in the request options _Object_ to configure the context for secure __https__ requests:
{ciphers: value}
- _--req-secure-protocol_ is the value to assign to the following key in the request options _Object_ to configure the context for secure __https__ requests:
{secureProtocol: value}
- _--req-secure-curve_ is the value to assign to the following key in the request options _Object_ to configure the context for secure __https__ requests:
{ecdhCurve: value}
- _--hooks_ is the filepath to a CommonJS module that exports a single JSON _Object_
"add_request_options": (url, is_m3u8) => request_options
* conditionally add HTTP request options
* inputs:
* url
* string URL
* is_m3u8
* boolean that indicates whether url will request an HLS manifest
* return value:
* _Object_ having attributes that are combined with _--req-options_ and used to send the outbound request to url
* "add_request_headers": (url, is_m3u8) => request_headers
* conditionally add HTTP request headers
* return value:
* _Object_ containing key:value pairs that are combined with _--req-headers_
* each _key_ is the name of an HTTP header to send in the outbound request to url
* "modify_m3u8_content": (m3u8_content, m3u8_url) => new_m3u8_content
* conditionally modify the content of .m3u8 files __before__ they are parsed to extract URLs
* "redirect": (url) => new_url
* conditionally redirect the URLs encountered in .m3u8 files __before__ they are modified to pass through the proxy
* "redirect_final": (url) => new_url
* conditionally redirect the URLs encountered in .m3u8 files __after__ they are modified to pass through the proxy
* "rewrite": (url) => new_url
* conditionally rewrite the URLs requested by clients __before__ they are proxied
* "prefetch": (url) => boolean
* conditionally decide whether to prefetch video segments on a per-URL basis
* return value must be a strict boolean type (ie: true or false)
* otherwise, the default behavior supersedes
- to only prefetch .ts files
* "prefetch_segments": (prefetch_urls, max_segments, is_vod, seg_duration_ms, perform_prefetch) => new_prefetch_urls
* conditionally filter the list of video segment URLs that are pending prefetch, when more than _--max-segments_ are contained in an HLS manifest
* inputs:
* prefetch_urls
* array of string video segment URLs
* max_segments
* integer that denotes the max length of the return value
* is_vod
* boolean that indicates whether the HLS manifest is for video-on-demand
* if true:
* the video is not a live stream
* the HLS manifest is complete and contains URLs for every video segment that would be needed to play the entire stream from start to finish
* seg_duration_ms
* integer that represents the duration (ms) of each video segment in the HLS manifest
* perform_prefetch
* function that accepts an array of string video segment URLs, and immediately begins to prefetch all corresponding segments
* return value:
* array of string video segment URLs that is a subset of prefetch_urls
* can be emtpy (ex: when using perform_prefetch)
* pre-conditions:
* the length of prefetch_urls is > max_segments
* post-conditions:
* the length of the return value array is <= max_segments
* "request_intervals": (add_request_interval) => {}
* enables the use of a cookie jar for all outbound HTTP requests
* adds any number of timers that each execute at individually specified intervals
* when each timer executes, it is passed an HTTP request client that is preconfigured to:
- include the request headers that are specified by other relevant options
- utilize the same cookie jar as all other outbound HTTP requests
* this allows the implementation of custom logic that may be required by one or more video hosts to periodically refresh or update session cookies
* an example would better illustrate usage:
module.exports = {
request_intervals: (add_request_interval) => {
add_request_interval(
0, // run timer once at startup to initialize cookies
(request) => {
request('https://example.com/login', {username: 'me', password: '12345'})
}
)
add_request_interval(
(1000 60 5), // run timer at 5 minute intervals to refresh cookies
(request, timer_id) => {
request('https://example.com/heart-beat')
if (globalThis.someCondition)
clearInterval(timer_id)
},
(1000 60 60 * 1) // automatically stop after a max duration of 1 hour
)
}
}
* more advanced configuration of the call to the HTTP request client is possible
- the 1st parameter is required, and must be a _URL_ string
- the 2nd parameter is optional, and can contain POST data
- the 3rd parameter is optional, and can be used for more advanced configuration options
* usage of this HTTP request client is documented here
- specifically, pay careful attention to the signatures for:
* the latter two input parameters
* the attributes of the Object that is resolved by the Promise in the return value (if the content of the response is needed)
- _--prefetch_ is a flag to enable the prefetch and caching of video segments
- _--max-segments_ is the maximum number of video segments (.ts files) to hold in each cache
20
- _--cache-timeout_ is the maximum number of seconds that any segment cache can remain unused before its contents are cleared (to reduce wasted space in memory)
60
- _--cache-key_ sets the type of string used to represent keys in the cache hashtable when logged
0 (default):
* sequence number of .ts file w/ .ts file extension (ex: "123.ts")
* pros:
* shortest type of string
* makes the log output easiest to read
* cons:
* in the wild, I've encountered video servers that assign each .ts file a unique filename that always terminate with the same static sequence number
* this is a strange edge case, but this option provides an easy workaround
* 1:
* full filename of .ts file
* 2:
* full URL of .ts file
- _--cache-storage_ selects a storage adapter that is used to hold the cache of prefetched video segments
memory (default):
* uses RAM
* filesystem:
* each video segment is written to a new file within a specified directory
* filenames are random and unique
- _--cache-storage-fs-dirpath_ specifies the directory in which to save video segments when using a filesystem-based cache storage adapter
filesystem
- _-v_ sets logging verbosity level:
-1:
* silent
* 0 (default):
* show errors only
* 1:
* show an informative amount of information
* 2:
* show technical details
* 3:
* show an enhanced technical trace (useful while debugging unexpected behavior)
* 4:
* show the content of .m3u8 files (both before and after URLs are modified)
- _--acl-ip_ restricts proxy server access to clients at IP addresses in whitelist
"192.168.1.100,192.168.1.101,192.168.1.102"
- _--acl-pass_ restricts proxy server access to requests that include a
passwordquerystring parameter having a value in whitelist
"1111,2222,3333,4444,5555"
- _--block-req-hostname_ blocks proxy server requests to hostnames in blacklist
"localhost,127.0.0.1"
- --http-proxy enables all outbound HTTP and HTTPS requests from HLS-Proxy to be tunnelled through an additional external web proxy server
http://myusername:[email protected]:1234
- _--tls-cert_ is the filepath to a security certificate to use for HTTPS
- _--tls-key_ is the filepath to the private key for the _--tls-cert_ security certificate
- _--tls-pass_ is the filepath to a text file containing the security passphrase for the _--tls-key_ private key
- _--manifest-extension_ is the file extension associated with HLS manifests
m3u8
- _--segment-extension_ is the file extension associated with media segments
ts
Examples:
- print help
hlsd --help
- print version
hlsd --version
- start HTTP proxy at default host:port
hlsd
- start HTTP proxy at default host and specific port
hlsd --port "8080"
- start HTTP proxy at specific host:port
hlsd --host "192.168.0.100" --port "8080"
- start HTTPS proxy at default host:port
hlsd --tls
- start HTTPS proxy at specific host:port
hlsd --tls --host "192.168.0.100" --port "8081"
- start HTTPS proxy at default host:port and send specific HTTP headers
hlsd --tls --req-headers "/path/to/request/headers.json"
- start HTTPS proxy at default host:port and enable prefetch of 10 video segments
hlsd --tls --prefetch --max-segments 10
- start HTTPS proxy using a non-generic security certificate
hlsd --tls-cert "/path/to/cert.pem" --tls-key "/path/to/key.pem" --tls-pass "/path/to/pass.phrase"
- - - -
Installation and Usage: Working with a Local git Repo
How to: Install:
git clone "https://github.com/warren-bank/node-HLS-Proxy.git"
cd "HLS-Proxy"
npm install
How to: Run the server(s):
# ----------------------------------------------------------------------
If using a port number >= 1024 on Linux, or
If using Windows:
----------------------------------------------------------------------
npm start [-- <options>]
----------------------------------------------------------------------
https://www.w3.org/Daemon/User/Installation/PrivilegedPorts.html
#
Linux considers port numbers < 1024 to be privileged.
Use "sudo":
----------------------------------------------------------------------
npm run sudo [-- <options>]
Options:
- identical to the command-line binary
Examples:
- print help
npm start -- --help
- start HTTP proxy at specific host:port
npm start -- --host "192.168.0.100" --port "8080"
- start HTTPS proxy at specific host:port
npm start -- --host "192.168.0.100" --port "8081" --tls
- start HTTP proxy at default host:port with escalated privilege
npm run sudo -- --port "80"
- start HTTPS proxy at default host:port with escalated privilege
npm run sudo -- --port "443" --tls
- start HTTP proxy at specific port and send custom "Referer" request header for specific video stream
npm start -- --port "8080"
h_referer='http://XXX:80/page.html'
URL='https://httpbin.org/headers'
URL="${URL}|${h_referer}"
URL=$(echo -n "$URL" | base64 --wrap=0)
URL="http://127.0.0.1:8080/${URL}.json"
URL='http://127.0.0.1:8080/aHR0cHM6Ly9odHRwYmluLm9yZy9oZWFkZXJzfGh0dHA6Ly9YWFg6ODAvcGFnZS5odG1s.json'
curl --silent "$URL"
- start HTTP proxy at specific port and send custom request headers
headers_file="${TMPDIR}/headers.json"
echo '{"Origin" : "http://XXX:80", "Referer": "http://XXX:80/page.html"}' > "$headers_file"
npm start -- --port "8080" --req-headers "$headers_file"
URL='https://httpbin.org/headers'
URL=$(echo -n "$URL" | base64 --wrap=0)
URL="http://127.0.0.1:8080/${URL}.json"
URL='http://127.0.0.1:8080/aHR0cHM6Ly9odHRwYmluLm9yZy9oZWFkZXJz.json'
curl --silent "$URL"
- start HTTPS proxy at specific port and send custom request headers
headers_file="${TMPDIR}/headers.json"
echo '{"Origin" : "http://XXX:80", "Referer": "http://XXX:80/page.html"}' > "$headers_file"
npm start -- --port "8081" --req-headers "$headers_file" --tls -v 1
URL='https://127.0.0.1:8081/aHR0cHM6Ly9odHRwYmluLm9yZy9oZWFkZXJz.json'
curl --silent --insecure "$URL"
- start HTTPS proxy at specific port and send custom request headers
h_origin='http://XXX:80'
h_referer='http://XXX:80/page.html'
h_useragent='Chromium'
h_custom_1='X-Foo: 123'
h_custom_2='X-Bar: baz'
npm start -- --port "8081" --origin "$h_origin" --referer "$h_referer" --useragent "$h_useragent" --header "$h_custom_1" --header "$h_custom_2" --tls -v 1
URL='https://127.0.0.1:8081/aHR0cHM6Ly9odHRwYmluLm9yZy9oZWFkZXJz.json'
curl --silent --insecure "$URL"
- - - -
Observations:
- when playing the proxied HLS video stream in an HTML5 player in a Chromium web browser (ex: THEOplayer)
NET::ERR_CERT_AUTHORITY_INVALID Your connection is not private
* click: Advanced
* click: Proceed to 127.0.0.1 (unsafe)
* done
- when playing the proxied HLS video stream on a Chromecast
Summary of (Rarely) Observed OpenSSL Connection Errors:
- error:
ssl3_check_cert_and_algorithm:dh key too small
1. attempted fix:--req-secure-ciphers "AES128-SHA"
- error:
SSL routines:SSL23_GET_SERVER_HELLO:sslv3 alert handshake failure
1. attempted fix:--req-secure-protocol "SSLv3_method"
* result:Error: SSLv3 methods disabled
* issue:
* node #3695
2. attempted fix:--req-secure-curve "auto"
- - - -
Other Projects:
##### (directly related, but very loosely coupled) * consists of 2 parts: 1. a Chromium web browser extension (.crx) * on each browser tab, it's silently watching the URL of all outbound requests * every requested URL matching a regex pattern that identifies it to be a video file is displayed in the modal window that toggles open when the extension's icon is clicked * links in this modal window open to URLs of component #2 2. a static website * there is a selection of several HTML5 videos players * each is better at some things and worse at others * each integrates with a different Chromecast receiver app * there is a page to help redirect the intercepted video URL through a local instance of _HLS Proxy_- - - -
Other Projects:
##### (unrelated, but somewhat similar in scope and purpose) * notes: * this project has __way__ more features, and is __way__ more polished * though its main purpose is to transcode online video with ffmpeg and pipe the output into another program, it can be configured to not load a video player and instead start a web server * it can strongly support individual websites through single-purpose plugins * it can also support streams via direct URLs * using URLs from the wild will have mixed results, since cookies and headers and authentication aren't being managed by any plugin * docs: * user guide * command-line usage * list of supported websites via plugins * binaries: * Windows portable * minimum system requirements: * Windows 7 SP1 * .NET Framework 4.5 * usage test: *streamlink --player-external-http --player-external-http-port 8080 --default-stream best --http-ignore-env --http-no-ssl-verify --url "https://XXX/video.m3u8"
* usage test result:
* doesn't appear to work with HTML5 video players or Chromecast
* the server starts and works as it was intended, but something about the format of the data it "streams" is incompatible
* VLC can play the video stream from the server, and be used to render the video on Chromecast
- - - -
Major Versions:
v1.x
v01
- summary:
* m3u8 manifest parser uses regex patterns to identify all URL patterns without any special knowledge of the m3u8 manifest specification
* internal proxy module exports a function that accepts an instance of http.Server and adds event listeners to process requests
- system requirements:
* Node.js version: v8.6.0 (and higher)
- transitive dependency requirements:
* v08.06.00+: @warren-bank/node-process-argv
* v08.06.00+: @warren-bank/node-request
v2.x
v02
- summary:
* m3u8 manifest parser uses regex patterns to identify all URL patterns without any special knowledge of the m3u8 manifest specification
* internal proxy module exports an Object containing event listeners to process requests that can be either:
- added to an instance of http.Server
- added to an Express.js application as middleware to handle a custom route
important limitation: since / is a valid character in a base64 encoded URL, the path for a custom route needs to end with a character that is not allowed in base64 encoding (ex: '/proxy_/')
- system requirements:
* Node.js version: v8.6.0 (and higher)
- transitive dependency requirements:
* v08.06.00+: @warren-bank/node-process-argv
* v08.06.00+: @warren-bank/node-request
v3.x
v03
- summary:
* m3u8 manifest parser uses special knowledge of the m3u8 manifest specification to contextually identify URLs
* internal proxy module exports an Object containing event listeners to process requests that can be either:
- added to an instance of http.Server
- added to an Express.js application as middleware to handle a custom route
important requirement: the path for a custom route needs to include exactly one unnamed parameter that matches the base64 encoded URL and (optionally) a file extension (ex: '/proxy/')
* the use of nested routers is supported
- system requirements:
* Node.js version: v8.6.0 (and higher)
- transitive dependency requirements:
* v08.06.00+: @warren-bank/node-process-argv
* v08.06.00+: @warren-bank/node-request
- - - -
Legal:
- copyright: Warren Bank
- license: GPL-2.0