diff --git a/LICENSE b/LICENSE index caca53d..130e443 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 -Copyright (C) 2020 Sam Zick +Copyright (C) 2017 Sam Zick Everyone is permitted to copy and distribute verbatim or modified copies of this license document, and changing it is allowed as long diff --git a/README.md b/README.md index be5e2ec..5679838 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

fHDHR_TEMPLATE Logo

+

fHDHR_Locast Logo

Welcome to the world of streaming content as a DVR device! We use some fancy python here to achieve a system of: diff --git a/config.all.ini b/config.all.ini index 8fcb979..8f92838 100644 --- a/config.all.ini +++ b/config.all.ini @@ -1,8 +1,8 @@ [main] # uuid = # cache_dir = -# servicename = TEMPLATE -# reponame = fHDHR_TEMPLATE +# servicename = Locast +# reponame = fHDHR_Locast [fhdhr] # address = 0.0.0.0 @@ -10,8 +10,8 @@ # port = 5004 # stream_type = direct # tuner_count = 4 -# friendlyname = fHDHR-TEMPLATE -# reporting_firmware_name = fHDHR_TEMPLATE +# friendlyname = fHDHR-Locast +# reporting_firmware_name = fHDHR_Locast # reporting_manufacturer = BoronDust # reporting_model = fHDHR # reporting_firmware_ver = 20201001 @@ -24,7 +24,7 @@ # update_frequency = 43200 [ffmpeg] -# path = ffmpeg +# ffmpeg_path = ffmpeg # bytes_per_read = 1152000 [vlc] @@ -41,5 +41,24 @@ # type = sqlite # driver = None -[template] +[locast] +# username = +# password = +# override_zipcode = None +# override_latitude = +# override_longitude = # force_best = False + +[zap2it] +# delay = 5 +# postalcode = None +# affiliate_id = gapzap +# country = USA +# device = - +# headendid = lineupId +# isoverride = True +# languagecode = en +# pref = +# timespan = 6 +# timezone = +# userid = - diff --git a/config.example.ini b/config.example.ini index f5837c7..30cb454 100644 --- a/config.example.ini +++ b/config.example.ini @@ -1,3 +1,7 @@ +[locast] +# username = +# password = + [fhdhr] # address = 0.0.0.0 # port = 5004 diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..57249b4 --- /dev/null +++ b/config.ini @@ -0,0 +1,39 @@ +[main] +uuid = dymunyqo + +[fhdhr] +address = 199.254.167.1 +port = 5006 +stream_type = ffmpeg +tuner_count = 4 +friendlyname = fHDHR-Ceton +reporting_firmware_name = fHDHR_Ceton +reporting_manufacturer = BoronDust +reporting_model = fHDHR +reporting_firmware_ver = 20201001 +reporting_tuner_type = Antenna +device_auth = fHDHR +chanscan_on_start = False + +[epg] +method = blocks + +[ffmpeg] + +[vlc] + +[direct_stream] + +[logging] +level = info + +[database] + +[ceton] +ceton_ip = 199.254.167.17 + +[zap2it] + +[web_ui] +advanced = False + diff --git a/docs/ADV_Config.md b/docs/ADV_Config.md index 2fbda6d..d9e6883 100644 --- a/docs/ADV_Config.md +++ b/docs/ADV_Config.md @@ -1,7 +1,7 @@

fHDHR Logo

--- -[Main](README.md) | [Setup and Usage](Usage.md) | [template](Origin.md) | [Credits/Related Projects](Related-Projects.md) +[Main](README.md) | [Setup and Usage](Usage.md) | [Locast](Origin.md) | [Credits/Related Projects](Related-Projects.md) --- **f**un **H**ome @@ -33,7 +33,7 @@ Here's the `main` section. The `fhdhr` contains all the configuration options for interfacing between this script and your media platform. * `address` and `port` are what we will allow the script to listen on. `0.0.0.0` is the default, and will respond to all. * `discovery_address` may be helpful for making SSDP work properly. If `address` is not `0.0.0.0`, we will use that. If this is not set to a real IP, we won't run SSDP. SSDP is only really helpful for discovering in Plex/Emby. It's a wasted resource since you can manually add the `ip:port` of the script to Plex. -* `tuner_count` is a limit of devices able to stream from the script. +* `tuner_count` is a limit of devices able to stream from the script. The default is 3, as per Locast's documentation. A 4th is possible, but is not reccomended. * `friendlyname` is to set the name that Plex sees the script as. * `stream_type` can be set to `ffmpeg`, `vlc` or `direct`. @@ -45,8 +45,8 @@ The `fhdhr` contains all the configuration options for interfacing between this # port = 5004 # stream_type = direct # tuner_count = 4 -# friendlyname = fHDHR-template -# reporting_firmware_name = fHDHR_TEMPLATE +# friendlyname = fHDHR-Locast +# reporting_firmware_name = fHDHR_Locast # reporting_manufacturer = BoronDust # reporting_model = fHDHR # reporting_firmware_ver = 20201001 @@ -56,7 +56,7 @@ The `fhdhr` contains all the configuration options for interfacing between this # EPG * `images` can be set to `proxy` or `pass`. If you choose `proxy`, images will be reverse proxied through fHDHR. -* `method` defaults to `origin` and will pull the xmltv data from template. Other Options include `blocks` which is an hourly schedule with minimal channel information. Another option is `zap2it`, which is another source of EPG information. Channel Numbers may need to be manually mapped. +* `method` defaults to `origin` and will pull the xmltv data from Locast. Other Options include `blocks` which is an hourly schedule with minimal channel information. Another option is `zap2it`, which is another source of EPG information. Channel Numbers may need to be manually mapped. * `update_frequency` * `epg_update_frequency` determines how often we check for new scheduling information. In Seconds. ```` @@ -119,11 +119,40 @@ TODO: improve documentation here. # driver = None ```` -## template -The `template` section +## Locast +The `locast` section +* requires `username` and `password`. The script will not run without these. +* `override_zipcode` is useful for if your DMA is not being picked up correctly. +* `override_latitude` and `override_longitude` are helpful for "mocking" your location. +* `force_best` will force select the best stream available. + ```` -[template] +[locast] # username = # password = +# override_zipcode = None +# mock_location = None +# force_best = False +```` + +## zap2it + +`zap2it` contains a ton of configuration options, and defaults to options that in my experience don't need to be adjusted. +* `postalcode` is a value of importance, and is helpful. If not set, the script will attempt to retrieve your postalcode automatically. + +```` +[zap2it] +# delay = 5 +# postalcode = None +# affiliate_id = gapzap +# country = USA +# device = - +# headendid = lineupId +# isoverride = True +# languagecode = en +# pref = +# timespan = 6 +# timezone = +# userid = - ```` diff --git a/docs/Config.md b/docs/Config.md index 16d6171..a3ff166 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -1,7 +1,7 @@

fHDHR Logo

--- -[Main](README.md) | [Setup and Usage](Usage.md) | [template](Origin.md) | [Credits/Related Projects](Related-Projects.md) +[Main](README.md) | [Setup and Usage](Usage.md) | [Locast](Origin.md) | [Credits/Related Projects](Related-Projects.md) --- **f**un **H**ome @@ -30,12 +30,12 @@ Under `fhdhr`, you'll find 2 addresses listed. `0.0.0.0` works great for a liste # discovery_address = 0.0.0.0 ```` -## template +## Locast -template requires signin credentials, so add those. +Locast requires signin credentials, so add those. ```` -[template] +[locast] # username = # password = ```` diff --git a/docs/Origin.md b/docs/Origin.md index a6876aa..8e2f9e1 100644 --- a/docs/Origin.md +++ b/docs/Origin.md @@ -1,7 +1,7 @@

fHDHR Logo

--- -[Main](README.md) | [Setup and Usage](Usage.md) | [template](Origin.md) | [Credits/Related Projects](Related-Projects.md) +[Main](README.md) | [Setup and Usage](Usage.md) | [Locast](Origin.md) | [Credits/Related Projects](Related-Projects.md) --- **f**un **H**ome @@ -12,4 +12,16 @@ --- -This varient of fHDHR connects to [template](https://template.com/about). +This varient of fHDHR connects to [Locast](https://www.locast.org/about). + +Usage with fHDHR requires: + +* an account with their service +* an active donation +* The service to be available in your DMA (“Designated Market Area”) + +Do NOT use fHDHR to obtain channels outside of your DMA. You will recieve no support for doing so. +This includes but is not limited to: + +* VPN services +* Cloud Server Hosting diff --git a/docs/README.md b/docs/README.md index 4212d52..3d1c221 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,7 +1,7 @@

fHDHR Logo

--- -[Main](README.md) | [Setup and Usage](Usage.md) | [template](Origin.md) | [Credits/Related Projects](Related-Projects.md) +[Main](README.md) | [Setup and Usage](Usage.md) | [Locast](Origin.md) | [Credits/Related Projects](Related-Projects.md) --- **f**un **H**ome diff --git a/docs/Related-Projects.md b/docs/Related-Projects.md index b1d8dc9..7d584aa 100644 --- a/docs/Related-Projects.md +++ b/docs/Related-Projects.md @@ -1,7 +1,7 @@

fHDHR Logo

--- -[Main](README.md) | [Setup and Usage](Usage.md) | [template](Origin.md) | [Credits/Related Projects](Related-Projects.md) +[Main](README.md) | [Setup and Usage](Usage.md) | [Locast](Origin.md) | [Credits/Related Projects](Related-Projects.md) --- **f**un **H**ome diff --git a/docs/Usage.md b/docs/Usage.md index 24df91e..0702b1a 100644 --- a/docs/Usage.md +++ b/docs/Usage.md @@ -1,7 +1,7 @@

fHDHR Logo

--- -[Main](README.md) | [Setup and Usage](Usage.md) | [template](Origin.md) | [Credits/Related Projects](Related-Projects.md) +[Main](README.md) | [Setup and Usage](Usage.md) | [Locast](Origin.md) | [Credits/Related Projects](Related-Projects.md) --- **f**un **H**ome @@ -50,72 +50,72 @@ fHDHR uses direct connections with video sources by default. Alternatively, you ## Docker This portion of the guide assumes you are using a Linux system with both docker and docker-compose installed. This (or some variation thereof) may work on Mac or Windows, but has not been tested. -* this guide assumes we wish to use the `~/fhdhr` directory for our install (you can use whatever directory you like, just make the appropriate changes elsewhere in this guide) and that we are installing for template support -* run the following commands to clone the repo into `~/fhdhr/fHDHR_TEMPLATE` +* this guide assumes we wish to use the `~/fhdhr` directory for our install (you can use whatever directory you like, just make the appropriate changes elsewhere in this guide) and that we are installing for Locast support +* run the following commands to clone the repo into `~/fhdhr/fHDHR_Locast` ``` cd ~/fhdhr -git clone https://github.com/fHDHR/fHDHR_TEMPLATE.git +git clone https://github.com/fHDHR/fHDHR_Locast.git ``` -* create your config.ini file (as described earlier in this guide) in the `~/fhdhr/fHDHR_TEMPLATE` directory +* create your config.ini file (as described earlier in this guide) in the `~/fhdhr/fHDHR_Locast` directory * while still in the `~/fhdhr` directory, create the following `docker-compose.yml` file ``` version: '3' services: - template: - build: ./fHDHR_TEMPLATE - container_name: template + locast: + build: ./fHDHR_Locast + container_name: locast network_mode: host volumes: - - ./fHDHR_TEMPLATE/config.ini:/app/config/config.ini + - ./fHDHR_Locast/config.ini:/app/config/config.ini ``` * run the following command to build and launch the container ``` -docker-compose up --build -d template +docker-compose up --build -d locast ``` After a short period of time (during which docker will build your new fHDHR container), you should now have a working build of fHDHR running inside a docker container. As the code changes and new versions / bug fixes are released, at any point you can pull the latest version of the code and rebuild your container with the following commands: ``` -cd ~/fhdhr/fHDHR_TEMPLATE +cd ~/fhdhr/fHDHR_Locast git checkout master git pull cd ~/fhdhr -docker-compose up --build -d template +docker-compose up --build -d locast ```
You can also run multiple instances of fHDHR to support additional sources by cloning the appropriate repo into your `~/fhdhr` directory and adding the necessary services to the docker-compose file we created above. -* for example, if we also wanted template support, you would clone the template repository: +* for example, if we also wanted PlutoTV support, you would clone the PlutoTV repository: ``` cd ~/fhdhr -git clone https://github.com/fHDHR/fHDHR_TEMPLATE.git +git clone https://github.com/fHDHR/fHDHR_PlutoTV.git ``` -* **NOTE**: if you are running multiple services on the same machine, you must change the port in your config.ini file for each one. For example, if template was using the default port of 5004, template cannot also use that port. You must change the port in your template config.ini file to something else (5005, for example). -* add template as a service in your `docker-compose.yml` file +* **NOTE**: if you are running multiple services on the same machine, you must change the port in your config.ini file for each one. For example, if Locast was using the default port of 5004, PlutoTV cannot also use that port. You must change the port in your PlutoTV config.ini file to something else (5005, for example). +* add plutotv as a service in your `docker-compose.yml` file ``` version: '3' services: - template: - build: ./fHDHR_TEMPLATE - container_name: template + locast: + build: ./fHDHR_Locast + container_name: locast network_mode: host volumes: - - ./fHDHR_TEMPLATE/config.ini:/app/config/config.ini + - ./fHDHR_Locast/config.ini:/app/config/config.ini - template: - build: ./fHDHR_TEMPLATE - container_name: template + plutotv: + build: ./fHDHR_PlutoTV + container_name: plutotv network_mode: host volumes: - - ./fHDHR_TEMPLATE/config.ini:/app/config/config.ini + - ./fHDHR_PlutoTV/config.ini:/app/config/config.ini ``` * run the following command to build and launch the container ``` -docker-compose up --build -d template +docker-compose up --build -d plutotv ``` You can repeat these instructions for as many fHDHR containers as your system resources will allow. diff --git a/docs/WebUI.md b/docs/WebUI.md index 4083423..957586d 100644 --- a/docs/WebUI.md +++ b/docs/WebUI.md @@ -1,7 +1,7 @@

fHDHR Logo

--- -[Main](README.md) | [Setup and Usage](Usage.md) | [template](Origin.md) | [Credits/Related Projects](Related-Projects.md) +[Main](README.md) | [Setup and Usage](Usage.md) | [Locast](Origin.md) | [Credits/Related Projects](Related-Projects.md) --- **f**un **H**ome @@ -27,7 +27,7 @@ Below is the main landing page with basic information. Main Page -# template +# Locast Here you will have access to some basic information about the service we are proxying. diff --git a/origin/__init__.py b/origin/__init__.py index 6014624..3ccf454 100644 --- a/origin/__init__.py +++ b/origin/__init__.py @@ -4,5 +4,5 @@ from .origin_channels import * from .origin_epg import * from .origin_web import * -ORIGIN_NAME = "fHDHR_Locast" +ORIGIN_NAME = "fHDHR_Ceton" ORIGIN_VERSION = "v0.5.0-beta" diff --git a/origin/origin_channels.py b/origin/origin_channels.py index 5440443..3020490 100644 --- a/origin/origin_channels.py +++ b/origin/origin_channels.py @@ -1,3 +1,7 @@ +import xmltodict +import base64 + +from random import randint class OriginChannels(): @@ -8,10 +12,137 @@ class OriginChannels(): def get_channels(self): - channel_list = [] + stations_url = ('http://' + self.fhdhr.config.dict["origin"]["ceton_ip"] + '/view_channel_map.cgi?page=0&xml=1') + url_headers = {'accept': 'application/xml;q=0.9, */*;q=0.8'} - return channel_list + try: + stationsReq = self.fhdhr.web.session.get(stations_url, headers=url_headers) + stationsReq.raise_for_status() + except self.fhdhr.web.exceptions.HTTPError as err: + self.fhdhr.logger.error('Error while getting stations: %s' % err) + return [] + + stationsRes = xmltodict.parse(stationsReq.content) + cleaned_channels = [] + + for station_item in stationsRes['channels']['channel']: + nameTmp = station_item["name"] + nameTmp_bytes = nameTmp.encode('ascii') + namebytes = base64.b64decode(nameTmp_bytes) + name = namebytes.decode('ascii') + clean_station_item = { + "name": name, + "callsign": name, + "number": station_item["number"], + "eia": station_item["eia"], + "id": station_item["sourceid"], + } + + cleaned_channels.append(clean_station_item) + return cleaned_channels + + def get_ceton_getvar(self, instance, query): + getVarUrl = ('http://' + self.fhdhr.config.dict["origin"]["ceton_ip"] + '/get_var?i=' + str(instance) + query) + + try: + getVarUrlReq = self.fhdhr.web.session.get(getVarUrl) + getVarUrlReq.raise_for_status() + except self.fhdhr.web.exceptions.HTTPError as err: + self.fhdhr.logger.error('Error while getting tuner variable for: %s - %s' % (query, err)) + return None + + return + + def get_ceton_tuner_status(self, chandict): + found = 0 + count = int(self.fhdhr.config.dict["fhdhr"]["tuner_count"]) + for instance in range(count): + getVarUrl = ('http://' + self.fhdhr.config.dict["origin"]["ceton_ip"] + '/get_var?i=' + str(instance) + '&s=av&v=TransportState') + + try: + getVarUrlReq = self.fhdhr.web.session.get(getVarUrl) + getVarUrlReq.raise_for_status() + except self.fhdhr.web.exceptions.HTTPError as err: + self.fhdhr.logger.error('Error while getting tuner status: %s' % err) + return None + + if "STOPPED" in getVarUrlReq.text: + self.fhdhr.logger.info('Tuner %s selected' % str(instance)) + found = 1 + break + + return found, instance + + def startstop_ceton_tuner(self, instance, startstop): + if not startstop: + port = 0 + self.fhdhr.logger.info('Tuner %s to be stopped' % str(instance)) + else: + port = randint(41001, 49999) + self.fhdhr.logger.info('Tuner %s to be started' % str(instance)) + + StartStopUrl = ('http://%s/stream_request.cgi' % self.fhdhr.config.dict["origin"]["ceton_ip"]) + + StartStop_data = {"instance_id": instance, + "dest_ip": self.fhdhr.config.dict["fhdhr"]["address"], + "dest_port": port, + "protocol": 0, + "start": startstop} + # StartStopUrl_headers = { + # 'Content-Type': 'application/json', + # 'User-Agent': "curl/7.64.1"} + + try: + StartStopUrlReq = self.fhdhr.web.session.post(StartStopUrl, StartStop_data) + StartStopUrlReq.raise_for_status() + except self.fhdhr.web.exceptions.HTTPError as err: + self.fhdhr.logger.error('Error while setting station stream: %s' % err) + return None + + return port + + def set_ceton_tuner(self, chandict, instance): + tuneChannelUrl = ('http://%s/channel_request.cgi' % self.fhdhr.config.dict["origin"]["ceton_ip"]) + tuneChannel_data = {"instance_id": 0, + "channel": chandict['number']} + + try: + tuneChannelUrlReq = self.fhdhr.web.session.post(tuneChannelUrl, tuneChannel_data) + tuneChannelUrlReq.raise_for_status() + except self.fhdhr.web.exceptions.HTTPError as err: + self.fhdhr.logger.error('Error while tuning station URL: %s' % err) + return None + + return 1 def get_channel_stream(self, chandict): - streamurl = "" + # streamurl = "rtsp://admin:password1@199.254.167.199:554" + # print (streamurl) + # return streamurl + + found, instance = self.get_ceton_tuner_status(chandict) + + # 1 to start or 0 to stop + if found: + port = self.startstop_ceton_tuner(instance, 1) + self.fhdhr.logger.error('No tuners available') + else: + port = 0 + + if port: + tuned = self.set_ceton_tuner(chandict, instance) + self.fhdhr.logger.error('Preparing tuner ' + str(instance) + ' on port:' + str(port)) + else: + tuned = 0 + + self.get_ceton_getvar(instance, "&s=tuner&v=Frequency") + self.get_ceton_getvar(instance, "&s=mux&v=ProgramNumber") + self.get_ceton_getvar(instance, "&s=diag&v=CopyProtectionStatus") + + if tuned: + self.fhdhr.logger.error('Initiate streaming from tuner ' + str(instance)) + streamurl = "udp://127.0.0.1:" + str(port) + else: + streamurl = None + return streamurl diff --git a/origin/origin_conf.json b/origin/origin_conf.json index 75f2ad5..c99d81d 100644 --- a/origin/origin_conf.json +++ b/origin/origin_conf.json @@ -1,29 +1,39 @@ { "main":{ "servicename":{ - "value": "TEMPLATE", + "value": "Ceton", "config_file": false, "config_web": false }, "dictpopname":{ - "value": "template", + "value": "ceton", "config_file": false, "config_web": false }, "reponame":{ - "value": "fHDHR_TEMPLATE", + "value": "fHDHR_Ceton", + "config_file": false, + "config_web": false + }, + "required":{ + "value": "ceton/ceton_ip", "config_file": false, "config_web": false } }, "fhdhr":{ "friendlyname":{ - "value": "fHDHR-TEMPLATE", + "value": "fHDHR-Ceton", "config_file": true, "config_web": true }, + "tuner_count":{ + "value": 3, + "config_file": true, + "config_web": true + }, "reporting_firmware_name":{ - "value": "fHDHR_TEMPLATE", + "value": "fHDHR_Ceton", "config_file": true, "config_web": true } @@ -35,10 +45,16 @@ "config_web": true }, "valid_epg_methods":{ - "value": "None,blocks,origin", + "value": "None,blocks,origin,zap2it,tvtv", "config_file": false, "config_web": false } }, - "template":{} + "ceton":{ + "ceton_ip":{ + "value": "none", + "config_file": true, + "config_web": true + } + } } diff --git a/origin/origin_epg.py b/origin/origin_epg.py index bac6ded..525eb69 100644 --- a/origin/origin_epg.py +++ b/origin/origin_epg.py @@ -8,10 +8,4 @@ class OriginEPG(): def update_epg(self, fhdhr_channels): programguide = {} - for fhdhr_id in list(fhdhr_channels.list.keys()): - chan_obj = fhdhr_channels.list[fhdhr_id] - - if str(chan_obj.dict["number"]) not in list(programguide.keys()): - programguide[str(chan_obj.dict["number"])] = chan_obj.epgdict - return programguide diff --git a/origin/origin_rtp.py b/origin/origin_rtp.py new file mode 100644 index 0000000..eecd9fc --- /dev/null +++ b/origin/origin_rtp.py @@ -0,0 +1,187 @@ +import socket +# import re +import bitstring # if you don't have this from your linux distro, install with "pip install bitstring" + +# ********* (2) The routine for handling the RTP stream *********** + + +def digestpacket(st): + """ This routine takes a UDP packet, i.e. a string of bytes and .. + (a) strips off the RTP header + (b) adds NAL "stamps" to the packets, so that they are recognized as NAL's + (c) Concantenates frames + (d) Returns a packet that can be written to disk as such and that is recognized by stock media players as h264 stream + """ + startbytes = "\x00\x00\x00\x01" # this is the sequence of four bytes that identifies a NAL packet.. must be in front of every NAL packet. + + bt = bitstring.BitArray(bytes=st) # turn the whole string-of-bytes packet into a string of bits. Very unefficient, but hey, this is only for demoing. + lc = 12 # bytecounter + bc = 12 * 8 # bitcounter + + version = bt[0:2].uint # version + p = bt[3] # P + x = bt[4] # X + cc = bt[4:8].uint # CC + m = bt[9] # M + pt = bt[9:16].uint # PT + sn = bt[16:32].uint # sequence number + timestamp = bt[32:64].uint # timestamp + ssrc = bt[64:96].uint # ssrc identifier + # The header format can be found from: + # https://en.wikipedia.org/wiki/Real-time_Transport_Protocol + + lc = 12 # so, we have red twelve bytes + bc = 12 * 8 # .. and that many bits + + print("version, p, x, cc, m, pt", version, p, x, cc, m, pt) + print("sequence number, timestamp", sn, timestamp) + print("sync. source identifier", ssrc) + + # st=f.read(4*cc) # csrc identifiers, 32 bits (4 bytes) each + cids = [] + for i in range(cc): + cids.append(bt[bc:bc+32].uint) + bc += 32 + lc += 4 + print("csrc identifiers:", cids) + + if (x): + # this section haven't been tested.. might fail + hid = bt[bc:bc+16].uint + bc += 16 + lc += 2 + + hlen = bt[bc:bc+16].uint + bc += 16 + lc += 2 + + print("ext. header id, header len", hid, hlen) + + # hst = bt[bc:bc+32*hlen] + bc += 32 * hlen + lc += 4 * hlen + + # OK, now we enter the NAL packet, as described here: + # + # https://tools.ietf.org/html/rfc6184#section-1.3 + # + # Some quotes from that document: + # + """ + 5.3. NAL Unit Header Usage + + + The structure and semantics of the NAL unit header were introduced in + Section 1.3. For convenience, the format of the NAL unit header is + reprinted below: + + +---------------+ + |0|1|2|3|4|5|6|7| + +-+-+-+-+-+-+-+-+ + |F|NRI| Type | + +---------------+ + + This section specifies the semantics of F and NRI according to this + specification. + + """ + """ + Table 3. Summary of allowed NAL unit types for each packetization + mode (yes = allowed, no = disallowed, ig = ignore) + + Payload Packet Single NAL Non-Interleaved Interleaved + Type Type Unit Mode Mode Mode + ------------------------------------------------------------- + 0 reserved ig ig ig + 1-23 NAL unit yes yes no + 24 STAP-A no yes no + 25 STAP-B no no yes + 26 MTAP16 no no yes + 27 MTAP24 no no yes + 28 FU-A no yes yes + 29 FU-B no no yes + 30-31 reserved ig ig ig + """ + # This was also very usefull: + # http://stackoverflow.com/questions/7665217/how-to-process-raw-udp-packets-so-that-they-can-be-decoded-by-a-decoder-filter-i + # A quote from that: + """ + First byte: [ 3 NAL UNIT BITS | 5 FRAGMENT TYPE BITS] + Second byte: [ START BIT | RESERVED BIT | END BIT | 5 NAL UNIT BITS] + Other bytes: [... VIDEO FRAGMENT DATA...] + """ + + fb = bt[bc] # i.e. "F" + nri = bt[bc+1:bc+3].uint # "NRI" + # nlu0 = bt[bc:bc+3] # "3 NAL UNIT BITS" (i.e. [F | NRI]) + typ = bt[bc+3:bc+8].uint # "Type" + print("F, NRI, Type :", fb, nri, typ) + print("first three bits together :", bt[bc:bc+3]) + + if (typ == 7 or typ == 8): + # this means we have either an SPS or a PPS packet + # they have the meta-info about resolution, etc. + # more reading for example here: + # http://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set/ + if (typ == 7): + print(">>>>> SPS packet") + else: + print(">>>>> PPS packet") + return startbytes+st[lc:] + # .. notice here that we include the NAL starting sequence "startbytes" and the "First byte" + + bc += 8 + lc += 1 # let's go to "Second byte" + # ********* WE ARE AT THE "Second byte" ************ + # The "Type" here is most likely 28, i.e. "FU-A" + start = bt[bc] # start bit + end = bt[bc+2] # end bit + # nlu1 = bt[bc+3:bc+8] # 5 nal unit bits + + if (start): # OK, this is a first fragment in a movie frame + print(">>> first fragment found") + # nlu = nlu0 + nlu1 # Create "[3 NAL UNIT BITS | 5 NAL UNIT BITS]" + # head = startbytes+nlu.bytes # .. add the NAL starting sequence + lc += 1 # We skip the "Second byte" + if (start is False and end is False): # intermediate fragment in a sequence, just dump "VIDEO FRAGMENT DATA" + # head = "" + lc += 1 # We skip the "Second byte" + elif (end is True): # last fragment in a sequence, just dump "VIDEO FRAGMENT DATA" + # head = "" + print("<<<< last fragment found") + lc += 1 # We skip the "Second byte" + + if (typ == 28): # This code only handles "Type" = 28, i.e. "FU-A" + print("missing code here?") + else: + raise(Exception, "unknown frame type for this piece of s***") + + +# *********** (3) THE MAIN PROGRAM STARTS HERE **************** + + +s1 = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + +# unknown name `clientports` +# s1.bind(("", clientports[0])) # we open a port that is visible to the whole internet (the empty string "" takes care of that) +s1.settimeout(5) # if the socket is dead for 5 s., its thrown into trash +# further reading: +# https://wiki.python.org/moin/UdpCommunication + +print("** STRIPPING RTP INFO AND DUMPING INTO FILE **") + +# unknown name `fname` +# f = open(fname, 'w') + +# unoknown name rn +# for i in range(rn): +# print +# print +# recst = s1.recv(4096) +# print("read", len(recst), "bytes") +# st = digestpacket(recst) +# print("dumping", len(st), "bytes") +# f.write(st) +# f.close() + +s1.close() diff --git a/origin/origin_service.py b/origin/origin_service.py index d5c870c..a3608a0 100644 --- a/origin/origin_service.py +++ b/origin/origin_service.py @@ -1,3 +1,5 @@ +# import os +# import bitstring class OriginService(): diff --git a/origin/origin_web/__init__.py b/origin/origin_web/__init__.py index 4b6008e..f35f5d5 100644 --- a/origin/origin_web/__init__.py +++ b/origin/origin_web/__init__.py @@ -1,5 +1,6 @@ from .origin_api import Origin_API +from .origin_api_tools import Origin_API_Tools from .origin_html import Origin_HTML @@ -9,4 +10,5 @@ class fHDHR_Origin_Web(): self.fhdhr = fhdhr self.origin_api = Origin_API(fhdhr) + self.origin_api_tools = Origin_API_Tools(fhdhr) self.origin_html = Origin_HTML(fhdhr) diff --git a/origin/origin_web/origin_api_tools.py b/origin/origin_web/origin_api_tools.py new file mode 100644 index 0000000..a68b779 --- /dev/null +++ b/origin/origin_web/origin_api_tools.py @@ -0,0 +1,78 @@ +from flask import Response, request, redirect +import urllib.parse +import json + + +class Origin_API_Tools(): + endpoints = ["/api/origin/tools"] + endpoint_name = "api_origin_tools" + endpoint_methods = ["GET", "POST"] + + def __init__(self, fhdhr): + self.fhdhr = fhdhr + + def __call__(self, *args): + return self.get(*args) + + def get(self, *args): + + method = request.args.get('method', default="get", type=str) + + redirect_url = request.args.get('redirect', default=None, type=str) + + if method == "channels": + + dma = request.args.get('dma', default=self.fhdhr.originwrapper.originservice.location["DMA"], type=str) + stations_url = 'https://api.locastnet.org/api/watch/epg/%s' % dma + + try: + stationsReq = self.fhdhr.web.session.get(stations_url) + stationsReq.raise_for_status() + stationsRes = stationsReq.json() + except self.fhdhr.web.exceptions.HTTPError as err: + self.fhdhr.logger.error('Error while getting stations: %s' % err) + stationsRes = [{"error": 'Error while getting stations: %s' % err, "listings": []}] + + filtered_json = [] + for station_item in stationsRes: + station_item["listings"] = [] + filtered_json.append(station_item) + + stations_json = json.dumps(filtered_json, indent=4) + + return Response(status=200, + response=stations_json, + mimetype='application/json') + + elif method == "zipcode": + + zipcode = request.args.get('zipcode', default=None, type=str) + if not zipcode: + if redirect_url: + return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method)) + else: + return "%s Success" % method + + status_url = 'https://api.locastnet.org/api/watch/dma/zip/%s' % zipcode + + try: + statusReq = self.fhdhr.web.session.get(status_url) + statusReq.raise_for_status() + statusRes = statusReq.json() + except self.fhdhr.web.exceptions.HTTPError as err: + self.fhdhr.logger.error('Error while getting zipcode status: %s' % err) + statusRes = [{"error": 'Error while getting zipcode status: %s' % err, "listings": []}] + + status_json = json.dumps(statusRes, indent=4) + + return Response(status=200, + response=status_json, + mimetype='application/json') + + else: + return "%s Invalid Method" % method + + if redirect_url: + return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method)) + else: + return "%s Success" % method diff --git a/origin/origin_web/origin_html.py b/origin/origin_web/origin_html.py index 1163041..1788cb1 100644 --- a/origin/origin_web/origin_html.py +++ b/origin/origin_web/origin_html.py @@ -1,6 +1,9 @@ from flask import request, render_template_string import pathlib from io import StringIO +import datetime + +from fHDHR.tools import humanized_time class Origin_HTML(): @@ -21,7 +24,6 @@ class Origin_HTML(): if self.fhdhr.originwrapper.setup_success: origin_status_dict = {"Setup": "Success"} - origin_status_dict["Total Channels"] = len(self.fhdhr.device.channels.list) else: origin_status_dict = {"Setup": "Failed"} return render_template_string(self.template.getvalue(), request=request, fhdhr=self.fhdhr, origin_status_dict=origin_status_dict, list=list) diff --git a/requirements.txt b/requirements.txt index d685c0b..13439d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ requests gevent flask image -xmltodict m3u8 sqlalchemy pycryptodome