mirror of
https://github.com/fergalmoran/xtreamium.git
synced 2025-12-22 09:41:33 +00:00
Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
files
|
||||
certs
|
||||
frontend/.env.production
|
||||
2
backend/.gitignore
vendored
Normal file
2
backend/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.idea
|
||||
app/config.py
|
||||
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
6
backend/app/config.sample.py
Normal file
6
backend/app/config.sample.py
Normal file
@@ -0,0 +1,6 @@
|
||||
provider = dict(
|
||||
name="<name>",
|
||||
server="http://<server>",
|
||||
username="<username>",
|
||||
password="<password>"
|
||||
)
|
||||
0
backend/app/lib/__init__.py
Normal file
0
backend/app/lib/__init__.py
Normal file
2
backend/app/lib/cache.py
Normal file
2
backend/app/lib/cache.py
Normal file
@@ -0,0 +1,2 @@
|
||||
class Cache:
|
||||
authData = {}
|
||||
3
backend/app/lib/mpv.py
Normal file
3
backend/app/lib/mpv.py
Normal file
@@ -0,0 +1,3 @@
|
||||
class MPVPlayer:
|
||||
pass
|
||||
|
||||
19
backend/app/lib/streamer.py
Normal file
19
backend/app/lib/streamer.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from httpx import AsyncClient
|
||||
import ffmpeg_streaming
|
||||
|
||||
|
||||
class Streamer:
|
||||
@staticmethod
|
||||
async def receive_file(url):
|
||||
async with AsyncClient(follow_redirects=True) as client:
|
||||
response = await client.get(url)
|
||||
|
||||
async for dataBytes in response.aiter_bytes():
|
||||
yield dataBytes
|
||||
|
||||
@staticmethod
|
||||
async def receive_stream(url):
|
||||
async with AsyncClient(follow_redirects=True) as client:
|
||||
async with client.stream('GET', url) as response:
|
||||
async for dataBytes in response.aiter_bytes():
|
||||
yield dataBytes
|
||||
84
backend/app/lib/xtream.py
Normal file
84
backend/app/lib/xtream.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from enum import Enum
|
||||
|
||||
from app.lib.cache import Cache
|
||||
import requests
|
||||
|
||||
|
||||
class UrlNotCreatedException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class StreamType(Enum):
|
||||
LIVE = "LIVE",
|
||||
VOD = "VOD",
|
||||
SERIES = "SERIES"
|
||||
|
||||
|
||||
class XTream:
|
||||
_cache = Cache()
|
||||
|
||||
def __init__(self, server, username, password):
|
||||
self._server = server
|
||||
self._username = username
|
||||
self._password = password
|
||||
self._interface = self._authenticate()
|
||||
|
||||
def _authenticate(self):
|
||||
r = requests.get(self.__get_authenticate_url())
|
||||
self._cache.authData = r.json()
|
||||
return r
|
||||
|
||||
# TODO: use f strings
|
||||
def __get_authenticate_url(self):
|
||||
url = '%s/player_api.php?username=%s&password=%s' % (self._server, self._username, self._password)
|
||||
return url
|
||||
|
||||
def __get_live_categories_url(self):
|
||||
url = '%s/player_api.php?username=%s&password=%s&action=%s' % (
|
||||
self._server, self._username, self._password, 'get_live_categories')
|
||||
return url
|
||||
|
||||
def __get_live_streams_url(self):
|
||||
url = '%s/player_api.php?username=%s&password=%s&action=%s' % (
|
||||
self._server, self._username, self._password, 'get_live_categories')
|
||||
return url
|
||||
|
||||
def __get_live_streams_by_category_url(self, category_id):
|
||||
url = '%s/player_api.php?username=%s&password=%s&action=%s&category_id=%s' % (
|
||||
self._server, self._username, self._password, 'get_live_streams', category_id)
|
||||
return url
|
||||
|
||||
def get_categories(self, stream_type=StreamType.LIVE):
|
||||
url = ""
|
||||
if stream_type == StreamType.LIVE:
|
||||
url = self.__get_live_categories_url()
|
||||
# elif stream_type == StreamType.VOD:
|
||||
# url = get_vod_cat_url()
|
||||
# elif stream_type == StreamType.SERIES:
|
||||
# url = get_series_cat_url()
|
||||
|
||||
if url == "":
|
||||
raise UrlNotCreatedException("Unable to create URL")
|
||||
|
||||
r = requests.get(url)
|
||||
return r
|
||||
|
||||
def get_streams_for_category(self, category_id, stream_type=StreamType.LIVE):
|
||||
url = ""
|
||||
if stream_type == StreamType.LIVE:
|
||||
url = self.__get_live_streams_by_category_url(category_id)
|
||||
|
||||
if url == "":
|
||||
raise UrlNotCreatedException("Unable to create URL")
|
||||
|
||||
r = requests.get(url)
|
||||
return r
|
||||
|
||||
def get_live_stream_url(self, stream_id):
|
||||
return f"{self._server}/live/{self._username}/{self._password}/{stream_id}.ts"
|
||||
|
||||
def get_vod_stream_url(self, stream_id):
|
||||
return f"{self._server}/movie/{self._username}/{self._password}/{stream_id}.ts"
|
||||
|
||||
def get_series_stream_url(self, stream_id):
|
||||
return f"{self._server}/series/{self._username}/{self._password}/{stream_id}.ts"
|
||||
68
backend/app/main.py
Normal file
68
backend/app/main.py
Normal file
@@ -0,0 +1,68 @@
|
||||
import uvicorn
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
|
||||
from . import config
|
||||
from .lib.streamer import Streamer
|
||||
from .lib.xtream import XTream
|
||||
|
||||
provider = XTream(
|
||||
config.provider['server'],
|
||||
config.provider['username'],
|
||||
config.provider['password']
|
||||
)
|
||||
|
||||
app = FastAPI()
|
||||
origins = [
|
||||
"https://dev-streams.fergl.ie:3000",
|
||||
"https://streams.fergl.ie",
|
||||
"http://127.0.0.1:35729",
|
||||
"http://localhost:35729",
|
||||
"https://bitmovin.com",
|
||||
"https://players.akamai.com",
|
||||
]
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@app.get("/channels")
|
||||
async def channels():
|
||||
categories = provider.get_categories()
|
||||
return categories.json()
|
||||
|
||||
|
||||
@app.get("/streams/{category_id}")
|
||||
async def read_item(category_id):
|
||||
streams = provider.get_streams_for_category(category_id)
|
||||
return streams.json()
|
||||
|
||||
|
||||
@app.get("/live/stream/{stream_id}")
|
||||
async def get_live_stream(stream_id: str):
|
||||
url = provider.get_live_stream_url(stream_id)
|
||||
return StreamingResponse(Streamer.receive_stream(url), media_type="video/mp2t")
|
||||
|
||||
|
||||
@app.get("/live/stream/url/{stream_id}")
|
||||
async def get_live_stream(stream_id: str):
|
||||
url = provider.get_live_stream_url(stream_id)
|
||||
return {
|
||||
"url": url
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
uvicorn.run("api:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=True,
|
||||
ssl_keyfile="/etc/letsencrypt/live/fergl.ie/privkey.pem",
|
||||
ssl_certfile="/etc/letsencrypt/live/fergl.ie/fullchain.pem"
|
||||
)
|
||||
7
backend/docker/Dockerfile
Normal file
7
backend/docker/Dockerfile
Normal file
@@ -0,0 +1,7 @@
|
||||
FROM python:3.9
|
||||
WORKDIR /code
|
||||
COPY ../requirements.txt /code/requirements.txt
|
||||
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
||||
COPY ../app /code/app
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--proxy-headers", "--host", "0.0.0.0", "--port", "80"]
|
||||
5
backend/requirements.txt
Normal file
5
backend/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
requests~=2.27.1
|
||||
fastapi~=0.75.0
|
||||
httpx~=0.22.0
|
||||
python-ffmpeg-video-streaming
|
||||
uvicorn~=0.17.6
|
||||
9
backend/scripts/build_docker.sh
Executable file
9
backend/scripts/build_docker.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
docker context use default
|
||||
|
||||
docker build \
|
||||
-t fergalmoran/xtream-backend \
|
||||
-f docker/Dockerfile .
|
||||
|
||||
docker push fergalmoran/xtream-backend
|
||||
3
frontend/.dockerignore
Normal file
3
frontend/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
./node_modules
|
||||
./dist
|
||||
./vite.config.ts
|
||||
9
frontend/.env
Normal file
9
frontend/.env
Normal file
@@ -0,0 +1,9 @@
|
||||
BROWSER=none
|
||||
HTTPS=true
|
||||
SSL_CRT_FILE=/etc/letsencrypt/live/fergl.ie/cert.pem
|
||||
SSL_KEY_FILE=/etc/letsencrypt/live/fergl.ie/privkey.pem
|
||||
|
||||
_REACT_APP_API_URL=https://streams.fergl.ie:8000
|
||||
REACT_APP_API_URL=https://api.streams.fergl.ie
|
||||
REACT_APP_SERVER_URL=http://localhost:9531
|
||||
|
||||
25
frontend/.gitignore
vendored
Normal file
25
frontend/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
env.production
|
||||
.env.production
|
||||
19
frontend/Dockerfile
Normal file
19
frontend/Dockerfile
Normal file
@@ -0,0 +1,19 @@
|
||||
# pull official base image
|
||||
FROM node:14-alpine
|
||||
|
||||
# set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# add `/app/node_modules/.bin` to $PATH
|
||||
ENV PATH /app/node_modules/.bin:$PATH
|
||||
|
||||
# install app dependencies
|
||||
COPY package.json ./
|
||||
COPY yarn.lock ./
|
||||
RUN yarn install
|
||||
|
||||
# add app
|
||||
COPY . ./
|
||||
|
||||
# start app
|
||||
CMD ["yarn", "start"]
|
||||
46
frontend/README.md
Normal file
46
frontend/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Getting Started with Create React App
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `yarn start`
|
||||
|
||||
Runs the app in the development mode.\
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.\
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.\
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.\
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.\
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
22
frontend/docker/Dockerfile
Normal file
22
frontend/docker/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
||||
FROM node:17-buster as builder
|
||||
|
||||
ARG RUN_MODE=production
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json package.json
|
||||
COPY yarn.lock yarn.lock
|
||||
COPY .env.${RUN_MODE} .env
|
||||
|
||||
RUN yarn install
|
||||
COPY . .
|
||||
|
||||
ENV NODE_ENV=$RUN_MODE
|
||||
RUN yarn build --mode $RUN_MODE
|
||||
|
||||
FROM nginx:alpine
|
||||
ENV NODE_ENV=$RUN_MODE
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
COPY --from=builder /app/build /usr/share/nginx/html
|
||||
COPY --from=builder /app/docker/nginx/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
CMD [ "/bin/sh", "-c", "nginx -g 'daemon off;'" ]
|
||||
12
frontend/docker/nginx/nginx.conf
Normal file
12
frontend/docker/nginx/nginx.conf
Normal file
@@ -0,0 +1,12 @@
|
||||
server {
|
||||
listen 80;
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
51
frontend/package.json
Normal file
51
frontend/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "xtream-player",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@testing-library/user-event": "^13.2.1",
|
||||
"autoprefixer": "^10.4.4",
|
||||
"hls.js": "^1.1.5",
|
||||
"postcss": "^8.4.12",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-router-dom": "6",
|
||||
"react-scripts": "5.0.0",
|
||||
"tailwindcss": "^3.0.23",
|
||||
"typescript": "^4.4.2",
|
||||
"web-vitals": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/hls.js": "^1.0.0",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^16.7.13",
|
||||
"@types/react": "^17.0.20",
|
||||
"@types/react-dom": "^17.0.9"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
68
frontend/public/icons/play.svg
Normal file
68
frontend/public/icons/play.svg
Normal file
@@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px"
|
||||
y="0px"
|
||||
viewBox="0 0 512 512"
|
||||
style="enable-background:new 0 0 512 512;"
|
||||
xml:space="preserve">
|
||||
<circle style="fill:#1FCFC1;"
|
||||
cx="256"
|
||||
cy="256"
|
||||
r="245.801"/>
|
||||
<polygon style="fill:#F2F2F2;"
|
||||
points="195.825,391.629 376.351,256 195.825,120.371 "/>
|
||||
<g>
|
||||
<path style="fill:#4D4D4D;"
|
||||
d="M256,512c-68.381,0-132.667-26.628-181.019-74.98C26.628,388.667,0,324.38,0,256
|
||||
S26.628,123.333,74.981,74.98C123.333,26.628,187.619,0,256,0s132.667,26.628,181.019,74.98C485.372,123.333,512,187.62,512,256
|
||||
s-26.628,132.667-74.981,181.02C388.667,485.372,324.381,512,256,512z M256,20.398C126.089,20.398,20.398,126.089,20.398,256
|
||||
S126.089,491.602,256,491.602S491.602,385.911,491.602,256S385.911,20.398,256,20.398z"/>
|
||||
<path style="fill:#4D4D4D;"
|
||||
d="M195.824,401.828c-1.553,0-3.115-0.355-4.557-1.075c-3.458-1.727-5.641-5.26-5.641-9.124V120.371
|
||||
c0-3.864,2.185-7.397,5.641-9.124c3.458-1.726,7.593-1.351,10.685,0.97l180.526,135.629c2.564,1.927,4.073,4.948,4.073,8.154
|
||||
s-1.508,6.228-4.073,8.154L201.951,399.783C200.15,401.137,197.994,401.828,195.824,401.828z M206.024,140.791v230.418L359.371,256
|
||||
L206.024,140.791z"/>
|
||||
<path style="fill:#4D4D4D;"
|
||||
d="M256,473.243c-5.632,0-10.199-4.566-10.199-10.199c0-5.633,4.567-10.199,10.199-10.199
|
||||
c52.815,0,102.404-20.633,139.633-58.1c3.973-3.996,10.429-4.015,14.425-0.045c3.995,3.971,4.016,10.428,0.046,14.424
|
||||
C369.016,450.471,314.287,473.243,256,473.243z"/>
|
||||
<path style="fill:#4D4D4D;"
|
||||
d="M430.396,377.825c-1.886,0-3.793-0.522-5.498-1.617c-4.741-3.041-6.118-9.351-3.076-14.092
|
||||
c1.514-2.36,2.998-4.788,4.411-7.216c2.834-4.867,9.077-6.516,13.945-3.684c4.868,2.833,6.518,9.077,3.684,13.945
|
||||
c-1.56,2.681-3.201,5.363-4.873,7.97C437.043,376.168,433.754,377.825,430.396,377.825z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
BIN
frontend/public/icons/unknown-stream.png
Normal file
BIN
frontend/public/icons/unknown-stream.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
26
frontend/public/index.html
Normal file
26
frontend/public/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon"
|
||||
href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color"
|
||||
content="#000000" />
|
||||
<meta name="description"
|
||||
content="Web site created using create-react-app" />
|
||||
<link rel="apple-touch-icon"
|
||||
href="%PUBLIC_URL%/logo192.png" />
|
||||
<link rel="manifest"
|
||||
href="%PUBLIC_URL%/manifest.json" />
|
||||
<title>Xtream Player</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
BIN
frontend/public/logo192.png
Normal file
BIN
frontend/public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/public/logo512.png
Normal file
BIN
frontend/public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
frontend/public/manifest.json
Normal file
25
frontend/public/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
frontend/public/robots.txt
Normal file
3
frontend/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
137
frontend/public/test.html
Normal file
137
frontend/public/test.html
Normal file
@@ -0,0 +1,137 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
<title>Title</title>
|
||||
|
||||
<!-- #region CSS -->
|
||||
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB"
|
||||
crossorigin="anonymous">
|
||||
<link href="https://stackpath.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-wvfXpqpZZVQGK6TAh5PVlGOfQNHSoD2xbE+QkPxCAFlNEevoEH3Sl0sibVcOQVnN"
|
||||
crossorigin="anonymous">
|
||||
<style type="text/css">
|
||||
html {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin-bottom: 60px;
|
||||
}
|
||||
|
||||
body>.container-fluid,
|
||||
body>.container {
|
||||
padding: 60px 15px 0;
|
||||
}
|
||||
|
||||
.footer>.container-fluid,
|
||||
.footer>.container {
|
||||
padding-right: 15px;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
<!-- #endregion -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- #region Header -->
|
||||
<header>
|
||||
<nav class="navbar navbar-expand-md navbar-dark fixed-top bg-dark">
|
||||
<a class="navbar-brand"
|
||||
href="#">Title</a>
|
||||
<button class="navbar-toggler"
|
||||
type="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#navbarCollapse"
|
||||
aria-controls="navbarCollapse"
|
||||
aria-expanded="false"
|
||||
aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse"
|
||||
id="navbarCollapse">
|
||||
<ul class="mr-auto navbar-nav">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link"
|
||||
href="#">Home
|
||||
<span class="sr-only">(current)</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region Main Content -->
|
||||
<main role="main"
|
||||
class="container">
|
||||
<h1 class="mt-5">Title</h1>
|
||||
<p class="lead">
|
||||
<video height="600"
|
||||
src="https://streams.fergl.ie:8000/live/stream/175541"
|
||||
id="video"
|
||||
controls></video>
|
||||
</p>
|
||||
</main>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region Footer -->
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
<span class="text-muted">Place sticky footer content here.</span>
|
||||
</div>
|
||||
</footer>
|
||||
<!-- #endregion -->
|
||||
|
||||
<!-- #region Scripts -->
|
||||
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"
|
||||
integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-u/bQvRA/1bobcXlcEYpsEdFVK/vJs3+T+nXLsBYJthmdBuavHvAW6UsmqO2Gd/F9"
|
||||
crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/holder/2.9.4/holder.js"
|
||||
integrity="sha256-crfkMD0BL2TtiwpbIlXF/SVmGSvOtgbMM8GBkibVKyc="
|
||||
crossorigin="anonymous"></script>
|
||||
<!-- #endregion -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.1.5/hls.min.js"
|
||||
integrity="sha512-O83G0C/Ypje2c3LTYElrDXQaqtKKxtu8WKlMLEMoIFs9HDeI4rMlpnn9AX5xvR3PgJpwSEBrZpxSzfE1usZqiQ=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"></script>
|
||||
|
||||
|
||||
<script>
|
||||
var video = document.getElementById('video');
|
||||
if (Hls.isSupported()) {
|
||||
var hls = new Hls({
|
||||
debug: true,
|
||||
});
|
||||
hls.loadSource('https://streams.fergl.ie:8000/live/stream/175541');
|
||||
hls.attachMedia(video);
|
||||
hls.on(Hls.Events.MEDIA_ATTACHED, function () {
|
||||
video.muted = true;
|
||||
video.play();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
115
frontend/public/unknown-stream.svg
Normal file
115
frontend/public/unknown-stream.svg
Normal file
@@ -0,0 +1,115 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="700pt" height="700pt" version="1.1" viewBox="0 0 700 700" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<defs>
|
||||
<symbol id="w" overflow="visible">
|
||||
<path d="m18.766-1.125c-0.96875 0.5-1.9805 0.875-3.0312 1.125-1.043 0.25781-2.1367 0.39062-3.2812 0.39062-3.3984 0-6.0898-0.94531-8.0781-2.8438-1.9922-1.9062-2.9844-4.4844-2.9844-7.7344 0-3.2578 0.99219-5.8359 2.9844-7.7344 1.9883-1.9062 4.6797-2.8594 8.0781-2.8594 1.1445 0 2.2383 0.13281 3.2812 0.39062 1.0508 0.25 2.0625 0.625 3.0312 1.125v4.2188c-0.98047-0.65625-1.9453-1.1406-2.8906-1.4531-0.94922-0.3125-1.9492-0.46875-3-0.46875-1.875 0-3.3516 0.60547-4.4219 1.8125-1.0742 1.1992-1.6094 2.8555-1.6094 4.9688 0 2.1055 0.53516 3.7617 1.6094 4.9688 1.0703 1.1992 2.5469 1.7969 4.4219 1.7969 1.0508 0 2.0508-0.14844 3-0.45312 0.94531-0.3125 1.9102-0.80078 2.8906-1.4688z"/>
|
||||
</symbol>
|
||||
<symbol id="c" overflow="visible">
|
||||
<path d="m13.734-11.141c-0.4375-0.19531-0.87109-0.34375-1.2969-0.4375-0.41797-0.10156-0.83984-0.15625-1.2656-0.15625-1.2617 0-2.2305 0.40625-2.9062 1.2188-0.67969 0.80469-1.0156 1.9531-1.0156 3.4531v7.0625h-4.8906v-15.312h4.8906v2.5156c0.625-1 1.3438-1.7266 2.1562-2.1875 0.82031-0.46875 1.8008-0.70312 2.9375-0.70312 0.16406 0 0.34375 0.011719 0.53125 0.03125 0.19531 0.011719 0.47656 0.039062 0.84375 0.078125z"/>
|
||||
</symbol>
|
||||
<symbol id="a" overflow="visible">
|
||||
<path d="m17.641-7.7031v1.4062h-11.453c0.125 1.1484 0.53906 2.0078 1.25 2.5781 0.70703 0.57422 1.7031 0.85938 2.9844 0.85938 1.0312 0 2.082-0.14844 3.1562-0.45312 1.082-0.3125 2.1914-0.77344 3.3281-1.3906v3.7656c-1.1562 0.4375-2.3125 0.76562-3.4688 0.98438-1.1562 0.22656-2.3125 0.34375-3.4688 0.34375-2.7734 0-4.9297-0.70312-6.4688-2.1094-1.5312-1.4062-2.2969-3.3789-2.2969-5.9219 0-2.5 0.75391-4.4609 2.2656-5.8906 1.5078-1.4375 3.582-2.1562 6.2188-2.1562 2.4062 0 4.332 0.73047 5.7812 2.1875 1.4453 1.4492 2.1719 3.3828 2.1719 5.7969zm-5.0312-1.625c0-0.92578-0.27344-1.6719-0.8125-2.2344-0.54297-0.57031-1.25-0.85938-2.125-0.85938-0.94922 0-1.7188 0.26562-2.3125 0.79688s-0.96484 1.2969-1.1094 2.2969z"/>
|
||||
</symbol>
|
||||
<symbol id="k" overflow="visible">
|
||||
<path d="m9.2188-6.8906c-1.0234 0-1.793 0.17188-2.3125 0.51562-0.51172 0.34375-0.76562 0.85547-0.76562 1.5312 0 0.625 0.20703 1.1172 0.625 1.4688 0.41406 0.34375 0.98828 0.51562 1.7188 0.51562 0.92578 0 1.7031-0.32812 2.3281-0.98438 0.63281-0.66406 0.95312-1.4922 0.95312-2.4844v-0.5625zm7.4688-1.8438v8.7344h-4.9219v-2.2656c-0.65625 0.92969-1.3984 1.6055-2.2188 2.0312-0.82422 0.41406-1.8242 0.625-3 0.625-1.5859 0-2.8711-0.45703-3.8594-1.375-0.99219-0.92578-1.4844-2.1289-1.4844-3.6094 0-1.7891 0.61328-3.1016 1.8438-3.9375 1.2383-0.84375 3.1797-1.2656 5.8281-1.2656h2.8906v-0.39062c0-0.76953-0.30859-1.332-0.92188-1.6875-0.61719-0.36328-1.5703-0.54688-2.8594-0.54688-1.0547 0-2.0312 0.10547-2.9375 0.3125-0.89844 0.21094-1.7305 0.52344-2.5 0.9375v-3.7344c1.0391-0.25 2.0859-0.44141 3.1406-0.57812 1.0625-0.13281 2.125-0.20312 3.1875-0.20312 2.7578 0 4.75 0.54688 5.9688 1.6406 1.2266 1.0859 1.8438 2.8555 1.8438 5.3125z"/>
|
||||
</symbol>
|
||||
<symbol id="b" overflow="visible">
|
||||
<path d="m7.7031-19.656v4.3438h5.0469v3.5h-5.0469v6.5c0 0.71094 0.14062 1.1875 0.42188 1.4375s0.83594 0.375 1.6719 0.375h2.5156v3.5h-4.1875c-1.9375 0-3.3125-0.39844-4.125-1.2031-0.80469-0.8125-1.2031-2.1797-1.2031-4.1094v-6.5h-2.4219v-3.5h2.4219v-4.3438z"/>
|
||||
</symbol>
|
||||
<symbol id="j" overflow="visible">
|
||||
<path d="m12.766-13.078v-8.2031h4.9219v21.281h-4.9219v-2.2188c-0.66797 0.90625-1.4062 1.5703-2.2188 1.9844s-1.7578 0.625-2.8281 0.625c-1.8867 0-3.4336-0.75-4.6406-2.25-1.2109-1.5-1.8125-3.4258-1.8125-5.7812 0-2.3633 0.60156-4.2969 1.8125-5.7969 1.207-1.5 2.7539-2.25 4.6406-2.25 1.0625 0 2 0.21484 2.8125 0.64062 0.82031 0.42969 1.5664 1.0859 2.2344 1.9688zm-3.2188 9.9219c1.0391 0 1.8359-0.37891 2.3906-1.1406 0.55078-0.76953 0.82812-1.8828 0.82812-3.3438 0-1.457-0.27734-2.5664-0.82812-3.3281-0.55469-0.76953-1.3516-1.1562-2.3906-1.1562-1.043 0-1.8398 0.38672-2.3906 1.1562-0.55469 0.76172-0.82812 1.8711-0.82812 3.3281 0 1.4609 0.27344 2.5742 0.82812 3.3438 0.55078 0.76172 1.3477 1.1406 2.3906 1.1406z"/>
|
||||
</symbol>
|
||||
<symbol id="i" overflow="visible">
|
||||
<path d="m10.5-3.1562c1.0508 0 1.8516-0.37891 2.4062-1.1406 0.55078-0.76953 0.82812-1.8828 0.82812-3.3438 0-1.457-0.27734-2.5664-0.82812-3.3281-0.55469-0.76953-1.3555-1.1562-2.4062-1.1562-1.0547 0-1.8594 0.38672-2.4219 1.1562-0.55469 0.77344-0.82812 1.8828-0.82812 3.3281 0 1.4492 0.27344 2.5586 0.82812 3.3281 0.5625 0.77344 1.3672 1.1562 2.4219 1.1562zm-3.25-9.9219c0.67578-0.88281 1.4219-1.5391 2.2344-1.9688 0.82031-0.42578 1.7656-0.64062 2.8281-0.64062 1.8945 0 3.4453 0.75 4.6562 2.25 1.207 1.5 1.8125 3.4336 1.8125 5.7969 0 2.3555-0.60547 4.2812-1.8125 5.7812-1.2109 1.5-2.7617 2.25-4.6562 2.25-1.0625 0-2.0078-0.21094-2.8281-0.625-0.8125-0.42578-1.5586-1.0859-2.2344-1.9844v2.2188h-4.8906v-21.281h4.8906z"/>
|
||||
</symbol>
|
||||
<symbol id="h" overflow="visible">
|
||||
<path d="m0.34375-15.312h4.8906l4.125 10.391 3.5-10.391h4.8906l-6.4375 16.766c-0.64844 1.6953-1.4023 2.8828-2.2656 3.5625-0.86719 0.6875-2 1.0312-3.4062 1.0312h-2.8438v-3.2188h1.5312c0.83203 0 1.4375-0.13672 1.8125-0.40625 0.38281-0.26172 0.67969-0.73047 0.89062-1.4062l0.14062-0.42188z"/>
|
||||
</symbol>
|
||||
<symbol id="e" overflow="visible">
|
||||
<path d="m2.5781-20.406h5.875l7.4219 14v-14h4.9844v20.406h-5.875l-7.4219-14v14h-4.9844z"/>
|
||||
</symbol>
|
||||
<symbol id="g" overflow="visible">
|
||||
<path d="m2.3594-15.312h4.8906v15.312h-4.8906zm0-5.9688h4.8906v4h-4.8906z"/>
|
||||
</symbol>
|
||||
<symbol id="v" overflow="visible">
|
||||
<path d="m2.3594-21.281h4.8906v11.594l5.625-5.625h5.6875l-7.4688 7.0312 8.0625 8.2812h-5.9375l-5.9688-6.3906v6.3906h-4.8906z"/>
|
||||
</symbol>
|
||||
<symbol id="f" overflow="visible">
|
||||
<path d="m17.75-9.3281v9.3281h-4.9219v-7.1094c0-1.3438-0.03125-2.2656-0.09375-2.7656s-0.16797-0.86719-0.3125-1.1094c-0.1875-0.3125-0.44922-0.55469-0.78125-0.73438-0.32422-0.17578-0.69531-0.26562-1.1094-0.26562-1.0234 0-1.8242 0.39844-2.4062 1.1875-0.58594 0.78125-0.875 1.8711-0.875 3.2656v7.5312h-4.8906v-21.281h4.8906v8.2031c0.73828-0.88281 1.5195-1.5391 2.3438-1.9688 0.83203-0.42578 1.75-0.64062 2.75-0.64062 1.7695 0 3.1133 0.54688 4.0312 1.6406 0.91406 1.0859 1.375 2.6562 1.375 4.7188z"/>
|
||||
</symbol>
|
||||
<symbol id="u" overflow="visible">
|
||||
<path d="m2.3594-21.281h4.8906v21.281h-4.8906z"/>
|
||||
</symbol>
|
||||
<symbol id="t" overflow="visible">
|
||||
<path d="m7.8281-16.438v12.453h1.8906c2.1562 0 3.8008-0.53125 4.9375-1.5938 1.1328-1.0625 1.7031-2.6133 1.7031-4.6562 0-2.0195-0.57031-3.5547-1.7031-4.6094-1.125-1.0625-2.7734-1.5938-4.9375-1.5938zm-5.25-3.9688h5.5469c3.0938 0 5.3984 0.21875 6.9219 0.65625 1.5195 0.4375 2.8203 1.1875 3.9062 2.25 0.95703 0.91797 1.6641 1.9805 2.125 3.1875 0.46875 1.1992 0.70312 2.5586 0.70312 4.0781 0 1.543-0.23438 2.918-0.70312 4.125-0.46094 1.2109-1.168 2.2773-2.125 3.2031-1.0938 1.0547-2.4062 1.8047-3.9375 2.25-1.5312 0.4375-3.8281 0.65625-6.8906 0.65625h-5.5469z"/>
|
||||
</symbol>
|
||||
<symbol id="s" overflow="visible">
|
||||
<path d="m0.42188-15.312h4.8906l3.8281 10.578 3.7969-10.578h4.9062l-6.0312 15.312h-5.375z"/>
|
||||
</symbol>
|
||||
<symbol id="r" overflow="visible">
|
||||
<path d="m12.422-21.281v3.2188h-2.7031c-0.6875 0-1.1719 0.125-1.4531 0.375-0.27344 0.25-0.40625 0.6875-0.40625 1.3125v1.0625h4.1875v3.5h-4.1875v11.812h-4.8906v-11.812h-2.4375v-3.5h2.4375v-1.0625c0-1.6641 0.46094-2.8984 1.3906-3.7031 0.92578-0.80078 2.3672-1.2031 4.3281-1.2031z"/>
|
||||
</symbol>
|
||||
<symbol id="d" overflow="visible">
|
||||
<path d="m9.6406-12.188c-1.0859 0-1.9141 0.39062-2.4844 1.1719-0.57422 0.78125-0.85938 1.9062-0.85938 3.375s0.28516 2.5938 0.85938 3.375c0.57031 0.77344 1.3984 1.1562 2.4844 1.1562 1.0625 0 1.875-0.38281 2.4375-1.1562 0.57031-0.78125 0.85938-1.9062 0.85938-3.375s-0.28906-2.5938-0.85938-3.375c-0.5625-0.78125-1.375-1.1719-2.4375-1.1719zm0-3.5c2.6328 0 4.6914 0.71484 6.1719 2.1406 1.4766 1.418 2.2188 3.3867 2.2188 5.9062 0 2.5117-0.74219 4.4805-2.2188 5.9062-1.4805 1.418-3.5391 2.125-6.1719 2.125-2.6484 0-4.7148-0.70703-6.2031-2.125-1.4922-1.4258-2.2344-3.3945-2.2344-5.9062 0-2.5195 0.74219-4.4883 2.2344-5.9062 1.4883-1.4258 3.5547-2.1406 6.2031-2.1406z"/>
|
||||
</symbol>
|
||||
<symbol id="q" overflow="visible">
|
||||
<path d="m16.547-12.766c0.61328-0.94531 1.3477-1.6719 2.2031-2.1719 0.85156-0.5 1.7891-0.75 2.8125-0.75 1.7578 0 3.0977 0.54688 4.0156 1.6406 0.92578 1.0859 1.3906 2.6562 1.3906 4.7188v9.3281h-4.9219v-7.9844-0.35938c0.007813-0.13281 0.015625-0.32031 0.015625-0.5625 0-1.082-0.16406-1.8633-0.48438-2.3438-0.3125-0.48828-0.82422-0.73438-1.5312-0.73438-0.92969 0-1.6484 0.38672-2.1562 1.1562-0.51172 0.76172-0.77344 1.8672-0.78125 3.3125v7.5156h-4.9219v-7.9844c0-1.6953-0.14844-2.7852-0.4375-3.2656-0.29297-0.48828-0.8125-0.73438-1.5625-0.73438-0.9375 0-1.6641 0.38672-2.1719 1.1562-0.51172 0.76172-0.76562 1.8594-0.76562 3.2969v7.5312h-4.9219v-15.312h4.9219v2.2344c0.60156-0.86328 1.2891-1.5156 2.0625-1.9531 0.78125-0.4375 1.6406-0.65625 2.5781-0.65625 1.0625 0 2 0.25781 2.8125 0.76562 0.8125 0.51172 1.4258 1.2305 1.8438 2.1562z"/>
|
||||
</symbol>
|
||||
<symbol id="p" overflow="visible">
|
||||
<path d="m2.1875-5.9688v-9.3438h4.9219v1.5312c0 0.83594-0.007813 1.875-0.015625 3.125-0.011719 1.25-0.015625 2.0859-0.015625 2.5 0 1.2422 0.03125 2.1328 0.09375 2.6719 0.070313 0.54297 0.17969 0.93359 0.32812 1.1719 0.20703 0.32422 0.47266 0.57422 0.79688 0.75 0.32031 0.16797 0.69141 0.25 1.1094 0.25 1.0195 0 1.8203-0.39062 2.4062-1.1719 0.58203-0.78125 0.875-1.8672 0.875-3.2656v-7.5625h4.8906v15.312h-4.8906v-2.2188c-0.74219 0.89844-1.5234 1.5586-2.3438 1.9844-0.82422 0.41406-1.7344 0.625-2.7344 0.625-1.7617 0-3.1055-0.53906-4.0312-1.625-0.92969-1.082-1.3906-2.6602-1.3906-4.7344z"/>
|
||||
</symbol>
|
||||
<symbol id="o" overflow="visible">
|
||||
<path d="m17.75-9.3281v9.3281h-4.9219v-7.1406c0-1.3203-0.03125-2.2344-0.09375-2.7344s-0.16797-0.86719-0.3125-1.1094c-0.1875-0.3125-0.44922-0.55469-0.78125-0.73438-0.32422-0.17578-0.69531-0.26562-1.1094-0.26562-1.0234 0-1.8242 0.39844-2.4062 1.1875-0.58594 0.78125-0.875 1.8711-0.875 3.2656v7.5312h-4.8906v-15.312h4.8906v2.2344c0.73828-0.88281 1.5195-1.5391 2.3438-1.9688 0.83203-0.42578 1.75-0.64062 2.75-0.64062 1.7695 0 3.1133 0.54688 4.0312 1.6406 0.91406 1.0859 1.375 2.6562 1.375 4.7188z"/>
|
||||
</symbol>
|
||||
<symbol id="n" overflow="visible">
|
||||
<path d="m2.5781-20.406h8.7344c2.5938 0 4.582 0.57812 5.9688 1.7344 1.3945 1.1484 2.0938 2.7891 2.0938 4.9219 0 2.1367-0.69922 3.7812-2.0938 4.9375-1.3867 1.1562-3.375 1.7344-5.9688 1.7344h-3.4844v7.0781h-5.25zm5.25 3.8125v5.7031h2.9219c1.0195 0 1.8047-0.25 2.3594-0.75 0.5625-0.5 0.84375-1.2031 0.84375-2.1094 0-0.91406-0.28125-1.6172-0.84375-2.1094-0.55469-0.48828-1.3398-0.73438-2.3594-0.73438z"/>
|
||||
</symbol>
|
||||
<symbol id="m" overflow="visible">
|
||||
<path d="m2.3594-15.312h4.8906v15.031c0 2.0508-0.49609 3.6172-1.4844 4.7031-0.98047 1.082-2.4062 1.625-4.2812 1.625h-2.4219v-3.2188h0.85938c0.92578 0 1.5625-0.21094 1.9062-0.625 0.35156-0.41797 0.53125-1.2461 0.53125-2.4844zm0-5.9688h4.8906v4h-4.8906z"/>
|
||||
</symbol>
|
||||
<symbol id="l" overflow="visible">
|
||||
<path d="m14.719-14.828v3.9844c-0.65625-0.45703-1.3242-0.79688-2-1.0156-0.66797-0.21875-1.3594-0.32812-2.0781-0.32812-1.3672 0-2.4336 0.40234-3.2031 1.2031-0.76172 0.79297-1.1406 1.9062-1.1406 3.3438 0 1.4297 0.37891 2.543 1.1406 3.3438 0.76953 0.79297 1.8359 1.1875 3.2031 1.1875 0.75781 0 1.4844-0.10938 2.1719-0.32812 0.6875-0.22656 1.3203-0.56641 1.9062-1.0156v4c-0.76172 0.28125-1.5391 0.48828-2.3281 0.625-0.78125 0.14453-1.5742 0.21875-2.375 0.21875-2.7617 0-4.9219-0.70703-6.4844-2.125-1.5547-1.4141-2.3281-3.3828-2.3281-5.9062 0-2.5312 0.77344-4.5039 2.3281-5.9219 1.5625-1.4141 3.7227-2.125 6.4844-2.125 0.80078 0 1.5938 0.074219 2.375 0.21875 0.78125 0.13672 1.5547 0.35156 2.3281 0.64062z"/>
|
||||
</symbol>
|
||||
</defs>
|
||||
<g>
|
||||
<path d="m-1005.2 0h1036v554.4h-1036z"/>
|
||||
<path d="m588.68 103.72h-477.37c-18.531 0-33.59 18.039-33.59 40.27v272.02c0 22.266 15.059 40.27 33.59 40.27h477.37c18.535 0 33.594-18.008 33.594-40.27v-272.01c0-22.238-15.059-40.273-33.594-40.273zm-104.38 278.76c0 16.762-11.328 30.32-25.277 30.32l-311.24-0.003906c-13.945 0-25.277-13.559-25.277-30.32v-204.93c0-16.762 11.328-30.352 25.277-30.352h311.24c13.945 0 25.277 13.59 25.277 30.352v204.93zm69.148-51.992c-23.375 0-42.363-18.996-42.363-42.438 0-23.375 18.996-42.363 42.363-42.363 23.445 0 42.363 18.996 42.363 42.363 0 23.445-18.914 42.438-42.363 42.438z" fill-rule="evenodd"/>
|
||||
<path d="m241.23 188.95 157.68 91.055-157.68 91.086z"/>
|
||||
<use x="70" y="644" xlink:href="#w"/>
|
||||
<use x="90.550781" y="644" xlink:href="#c"/>
|
||||
<use x="104.359375" y="644" xlink:href="#a"/>
|
||||
<use x="123.347656" y="644" xlink:href="#k"/>
|
||||
<use x="142.242188" y="644" xlink:href="#b"/>
|
||||
<use x="155.628906" y="644" xlink:href="#a"/>
|
||||
<use x="174.617188" y="644" xlink:href="#j"/>
|
||||
<use x="204.410156" y="644" xlink:href="#i"/>
|
||||
<use x="224.453125" y="644" xlink:href="#h"/>
|
||||
<use x="252.453125" y="644" xlink:href="#e"/>
|
||||
<use x="275.886719" y="644" xlink:href="#g"/>
|
||||
<use x="285.484375" y="644" xlink:href="#v"/>
|
||||
<use x="304.105469" y="644" xlink:href="#f"/>
|
||||
<use x="324.039062" y="644" xlink:href="#g"/>
|
||||
<use x="333.632812" y="644" xlink:href="#u"/>
|
||||
<use x="352.980469" y="644" xlink:href="#t"/>
|
||||
<use x="376.222656" y="644" xlink:href="#a"/>
|
||||
<use x="395.210938" y="644" xlink:href="#s"/>
|
||||
<use x="70" y="672" xlink:href="#r"/>
|
||||
<use x="82.183594" y="672" xlink:href="#c"/>
|
||||
<use x="95.992188" y="672" xlink:href="#d"/>
|
||||
<use x="115.226562" y="672" xlink:href="#q"/>
|
||||
<use x="154.152344" y="672" xlink:href="#b"/>
|
||||
<use x="167.535156" y="672" xlink:href="#f"/>
|
||||
<use x="187.46875" y="672" xlink:href="#a"/>
|
||||
<use x="216.207031" y="672" xlink:href="#e"/>
|
||||
<use x="239.640625" y="672" xlink:href="#d"/>
|
||||
<use x="258.878906" y="672" xlink:href="#p"/>
|
||||
<use x="278.8125" y="672" xlink:href="#o"/>
|
||||
<use x="308.492188" y="672" xlink:href="#n"/>
|
||||
<use x="329.015625" y="672" xlink:href="#c"/>
|
||||
<use x="342.820312" y="672" xlink:href="#d"/>
|
||||
<use x="362.058594" y="672" xlink:href="#m"/>
|
||||
<use x="371.65625" y="672" xlink:href="#a"/>
|
||||
<use x="390.648438" y="672" xlink:href="#l"/>
|
||||
<use x="407.242188" y="672" xlink:href="#b"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
9
frontend/scripts/build_docker.sh
Executable file
9
frontend/scripts/build_docker.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
docker context use default
|
||||
|
||||
docker build \
|
||||
-t fergalmoran/xtream-frontend \
|
||||
-f docker/Dockerfile .
|
||||
|
||||
docker push fergalmoran/xtream-frontend
|
||||
26
frontend/src/App.tsx
Normal file
26
frontend/src/App.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React from "react";
|
||||
import { Sidebar } from "./components";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { HomePage, ChannelPage, PlayerPage } from "./pages";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="flex flex-col h-screen font-Rampart">
|
||||
<div className="flex flex-1 overflow-hidden">
|
||||
<Sidebar />
|
||||
<div className="flex flex-col flex-1">
|
||||
<div className="flex h-16 p-4 bg-gray-100">Header</div>
|
||||
<main className="flex flex-1 px-4 pt-5 overflow-y-auto">
|
||||
<Routes>
|
||||
<Route path="/" element={<HomePage />} />
|
||||
<Route path="live/channel/:channelId" element={<ChannelPage />} />
|
||||
<Route path="live/play/:streamId" element={<PlayerPage />} />
|
||||
</Routes>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
88
frontend/src/components/hls-player.component.tsx
Normal file
88
frontend/src/components/hls-player.component.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React, { useEffect, RefObject } from "react";
|
||||
import Hls, { HlsConfig } from "hls.js";
|
||||
|
||||
export interface HlsPlayerProps extends React.VideoHTMLAttributes<HTMLVideoElement> {
|
||||
hlsConfig?: Partial<HlsConfig>;
|
||||
playerRef?: RefObject<HTMLVideoElement>;
|
||||
src: string;
|
||||
}
|
||||
|
||||
function HLSPlayer({
|
||||
hlsConfig,
|
||||
playerRef = React.createRef<HTMLVideoElement>(),
|
||||
src,
|
||||
autoPlay,
|
||||
...props
|
||||
}: HlsPlayerProps) {
|
||||
useEffect(() => {
|
||||
let hls: Hls;
|
||||
|
||||
function _initPlayer() {
|
||||
if (hls != null) {
|
||||
hls.destroy();
|
||||
}
|
||||
|
||||
const newHls = new Hls({
|
||||
enableWorker: false,
|
||||
...hlsConfig,
|
||||
});
|
||||
|
||||
if (playerRef.current != null) {
|
||||
newHls.attachMedia(playerRef.current);
|
||||
}
|
||||
|
||||
newHls.on(Hls.Events.MEDIA_ATTACHED, () => {
|
||||
newHls.loadSource(src);
|
||||
|
||||
newHls.on(Hls.Events.MANIFEST_PARSED, () => {
|
||||
if (autoPlay) {
|
||||
playerRef?.current
|
||||
?.play()
|
||||
.catch(() =>
|
||||
console.log(
|
||||
"Unable to autoplay prior to user interaction with the dom."
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
newHls.on(Hls.Events.ERROR, function (event, data) {
|
||||
if (data.fatal) {
|
||||
switch (data.type) {
|
||||
case Hls.ErrorTypes.NETWORK_ERROR:
|
||||
newHls.startLoad();
|
||||
break;
|
||||
case Hls.ErrorTypes.MEDIA_ERROR:
|
||||
newHls.recoverMediaError();
|
||||
break;
|
||||
default:
|
||||
_initPlayer();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
hls = newHls;
|
||||
}
|
||||
|
||||
// Check for Media Source support
|
||||
if (Hls.isSupported()) {
|
||||
_initPlayer();
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (hls != null) {
|
||||
hls.destroy();
|
||||
}
|
||||
};
|
||||
}, [autoPlay, hlsConfig, playerRef, src]);
|
||||
|
||||
// If Media Source is supported, use HLS.js to play video
|
||||
if (Hls.isSupported()) return <video ref={playerRef} {...props} />;
|
||||
|
||||
// Fallback to using a regular video player if HLS is supported by default in the user's browser
|
||||
return <video ref={playerRef} src={src} autoPlay={autoPlay} {...props} />;
|
||||
}
|
||||
|
||||
export default HLSPlayer;
|
||||
5
frontend/src/components/index.ts
Normal file
5
frontend/src/components/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import HLSPlayer from "./hls-player.component";
|
||||
import Navbar from "./navbar.component";
|
||||
import Sidebar from "./sidebar.component";
|
||||
|
||||
export { Navbar, Sidebar, HLSPlayer };
|
||||
100
frontend/src/components/navbar.component.tsx
Normal file
100
frontend/src/components/navbar.component.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
|
||||
const Navbar = () => {
|
||||
return (
|
||||
<nav className="fixed z-30 w-full bg-white border-b-2 border-indigo-600">
|
||||
<div className="px-6 py-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-start">
|
||||
<button className="p-2 text-gray-600 rounded cursor-pointer lg:hidden ">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<Link to="/" className="flex items-center text-xl font-bold">
|
||||
<span className="text-blue-800">Xtream Player</span>
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="hidden mr-6 lg:block">
|
||||
<form action="#">
|
||||
<label className="sr-only">Search</label>
|
||||
<div className="relative mt-1 lg:w-64">
|
||||
<div className="absolute inset-y-0 left-0 flex items-center pl-3">
|
||||
<svg
|
||||
className="w-5 h-5 text-gray-500"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
className=" border text-gray-900 sm:text-sm rounded-lg focus:outline-none focus:ring-1 block w-full pl-10 p-2.5"
|
||||
placeholder="Search"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="w-6 h-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="relative inline-block ">
|
||||
{/* Dropdown toggle button */}
|
||||
<button className="relative flex items-center p-2 text-sm text-gray-600 bg-white border border-transparent rounded-md focus:border-blue-500 focus:ring-opacity-40 dark:focus:ring-opacity-40 focus:ring-blue-300 dark:focus:ring-blue-400 focus:ring dark:text-white dark:bg-gray-800 focus:outline-none">
|
||||
<span className="mx-1">Jane Doe</span>
|
||||
<svg
|
||||
className="w-5 h-5 mx-1"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 15.713L18.01 9.70299L16.597 8.28799L12 12.888L7.40399 8.28799L5.98999 9.70199L12 15.713Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
{/* Dropdown menu */}
|
||||
<div className="absolute right-0 z-20 w-56 mt-2 overflow-hidden bg-white rounded-md"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Navbar;
|
||||
87
frontend/src/components/sidebar.component.tsx
Normal file
87
frontend/src/components/sidebar.component.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Channel } from "../models/channel";
|
||||
|
||||
const Sidebar = () => {
|
||||
const [channels, setChannels] = React.useState<Channel[]>([]);
|
||||
const [filteredChannels, setFilteredChannels] = React.useState<Channel[]>([]);
|
||||
React.useEffect(() => {
|
||||
const fetchChannels = async () => {
|
||||
const res = await fetch(`${process.env.REACT_APP_API_URL}/channels`);
|
||||
const data = await res.json();
|
||||
setChannels(data);
|
||||
setFilteredChannels(data);
|
||||
};
|
||||
|
||||
fetchChannels().catch(console.error);
|
||||
}, []);
|
||||
const _searchChannels = ($event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const searchString = $event.target.value;
|
||||
if (searchString) {
|
||||
const filteredChannels = channels.filter((c) => {
|
||||
const result = c.category_name.toLowerCase().includes(searchString.toLowerCase());
|
||||
console.log(
|
||||
"sidebar.component",
|
||||
`Category Name: ${c.category_name}`,
|
||||
`Search String: ${searchString}`
|
||||
);
|
||||
console.log("sidebar.component", "Result", result);
|
||||
return result;
|
||||
});
|
||||
setFilteredChannels(filteredChannels);
|
||||
} else {
|
||||
setFilteredChannels(channels);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<aside className="flex flex-col w-1/5 pl-2 bg-gray-100">
|
||||
<div className="flex items-center justify-center h-16">
|
||||
<div className="flex p-8">
|
||||
<input
|
||||
type="text"
|
||||
className="px-4 py-2"
|
||||
placeholder="Search..."
|
||||
onChange={_searchChannels}
|
||||
/>
|
||||
<button className="flex items-center justify-center px-4 border-l">
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-600"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path d="M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul className="overflow-y-auto">
|
||||
<li>
|
||||
{filteredChannels.map((channel: Channel) => (
|
||||
<Link
|
||||
key={channel.category_id}
|
||||
to={`/live/channel/${channel.category_id}`}
|
||||
className="flex items-center p-2 text-base font-normal text-gray-900 rounded-lg dark:text-white hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<svg
|
||||
className="w-6 h-6 text-gray-500 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d="M2 10a8 8 0 018-8v8h8a8 8 0 11-16 0z" />
|
||||
<path d="M12 2.252A8.014 8.014 0 0117.748 8H12V2.252z" />
|
||||
</svg>
|
||||
<span className="ml-3 text-sm font-semibold">
|
||||
{channel.category_name}
|
||||
</span>
|
||||
</Link>
|
||||
))}
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
5
frontend/src/index.css
Normal file
5
frontend/src/index.css
Normal file
@@ -0,0 +1,5 @@
|
||||
@import url("https://fonts.googleapis.com/css2?family=Raleway&display=swap");
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
20
frontend/src/index.tsx
Normal file
20
frontend/src/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import "./index.css";
|
||||
import App from "./App";
|
||||
import reportWebVitals from "./reportWebVitals";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
// to log results (for example: reportWebVitals(console.log))
|
||||
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||
reportWebVitals();
|
||||
5
frontend/src/models/channel.ts
Normal file
5
frontend/src/models/channel.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Channel {
|
||||
category_id: number;
|
||||
category_name: string;
|
||||
parent_id: number;
|
||||
}
|
||||
14
frontend/src/models/stream.ts
Normal file
14
frontend/src/models/stream.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export interface Stream {
|
||||
num: number;
|
||||
name: string;
|
||||
stream_type: string;
|
||||
stream_id: number;
|
||||
stream_icon: string;
|
||||
epg_channel_id: null;
|
||||
added: number;
|
||||
category_id: number;
|
||||
custom_sid: string;
|
||||
tv_archive: number;
|
||||
direct_source: string;
|
||||
tv_archive_duration: number;
|
||||
}
|
||||
110
frontend/src/pages/channel.page.tsx
Normal file
110
frontend/src/pages/channel.page.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import React from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Stream } from "../models/stream";
|
||||
const ChannelPage = () => {
|
||||
let params = useParams();
|
||||
|
||||
const [streams, setStreams] = React.useState<Stream[]>([]);
|
||||
React.useEffect(() => {
|
||||
const fetchChannels = async () => {
|
||||
const res = await fetch(
|
||||
`${process.env.REACT_APP_API_URL}/streams/${params.channelId}`
|
||||
);
|
||||
const data = await res.json();
|
||||
setStreams(data);
|
||||
};
|
||||
|
||||
fetchChannels().catch(console.error);
|
||||
}, [params.channelId]);
|
||||
|
||||
const handleXHR = (...args: any[]) => {
|
||||
console.log("channel.page", "handleXHR", args);
|
||||
};
|
||||
const playStream = async (streamId: number) => {
|
||||
const res = await fetch(
|
||||
`${process.env.REACT_APP_API_URL}/live/stream/url/${streamId}`
|
||||
);
|
||||
if (res.status !== 200) {
|
||||
alert("Failed to get stream url");
|
||||
return;
|
||||
}
|
||||
const data = await res.json();
|
||||
if (data.url) {
|
||||
const mpv_args =
|
||||
"--keep-open=yes\n--geometry=1024x768-0-0\n--ontop\n--screen=2\n--ytdl-format=bestvideo[ext=mp4][height<=?720]+bestaudio[ext=m4a]\n--border=no".split(
|
||||
/\n/
|
||||
);
|
||||
|
||||
const query =
|
||||
`?play_url=` +
|
||||
encodeURIComponent(data.url) +
|
||||
[""].concat(mpv_args.map(encodeURIComponent)).join("&mpv_args=");
|
||||
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.onreadystatechange = handleXHR;
|
||||
xhr.open("GET", `${process.env.REACT_APP_SERVER_URL}/${query}`, true);
|
||||
xhr.send();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div className="flex flex-col w-full h-screen ">
|
||||
<table className="font-semibold leading-normal table-auto">
|
||||
<thead className="sticky top-0 font-semibold text-left text-white uppercase bg-indigo-500">
|
||||
<tr className="">
|
||||
<th className="text-lg uppercase border-b border-gray-200 1px-5">
|
||||
Channel name
|
||||
</th>
|
||||
<th className="border-b border-gray-200 1px-5">Type</th>
|
||||
<th className="py-3 border-b border-gray-200 1px-5"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y ">
|
||||
{streams.map((stream: Stream) => (
|
||||
<tr key={stream.num}>
|
||||
<td className="px-5 py-5 text-sm border-b border-gray-200">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<img
|
||||
alt="stream"
|
||||
src={
|
||||
stream.stream_icon ||
|
||||
`${process.env.PUBLIC_URL}/icons/unknown-stream.png`
|
||||
}
|
||||
className="object-cover w-10 h-10 mx-auto rounded-full "
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-gray-900 whitespace-no-wrap">
|
||||
{stream.name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td className="px-5 py-5 text-sm border-b border-gray-200">
|
||||
<span className="relative inline-block px-3 py-1 font-semibold leading-tight text-green-900">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="absolute inset-0 bg-green-200 rounded-full opacity-50"
|
||||
></span>
|
||||
<span className="relative">{stream.stream_type}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-5 py-5 text-sm border-b border-gray-200">
|
||||
<button onClick={() => playStream(stream.stream_id)}>
|
||||
<img
|
||||
className="w-10 h-10 "
|
||||
src={`${process.env.PUBLIC_URL}/icons/play.svg`}
|
||||
alt="Play"
|
||||
/>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelPage;
|
||||
7
frontend/src/pages/home.page.tsx
Normal file
7
frontend/src/pages/home.page.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
const HomePage = () => {
|
||||
return <div>HomePage</div>;
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
5
frontend/src/pages/index.ts
Normal file
5
frontend/src/pages/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import ChannelPage from "./channel.page";
|
||||
import HomePage from "./home.page";
|
||||
import PlayerPage from "./player.page";
|
||||
|
||||
export { HomePage, ChannelPage, PlayerPage };
|
||||
40
frontend/src/pages/player.page.tsx
Normal file
40
frontend/src/pages/player.page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { HLSPlayer } from "../components";
|
||||
|
||||
const PlayerPage = () => {
|
||||
const params = useParams();
|
||||
const [streamUrl, setStreamUrl] = React.useState("");
|
||||
React.useEffect(() => {
|
||||
setStreamUrl(
|
||||
`${process.env.REACT_APP_API_URL}/live/stream/${params.streamId}`
|
||||
);
|
||||
console.log("player.page", "streamUrl", streamUrl);
|
||||
}, [params.streamId, streamUrl]);
|
||||
return (
|
||||
<div>
|
||||
{streamUrl && (
|
||||
<>
|
||||
<span>
|
||||
<a href={streamUrl} target="_blank" rel="noreferrer">
|
||||
{streamUrl}
|
||||
</a>
|
||||
</span>
|
||||
<div className="w-96 h-96">
|
||||
<HLSPlayer
|
||||
src={streamUrl}
|
||||
hlsConfig={{
|
||||
debug: true,
|
||||
maxLoadingDelay: 4,
|
||||
minAutoBitrate: 0,
|
||||
lowLatencyMode: true,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlayerPage;
|
||||
1
frontend/src/react-app-env.d.ts
vendored
Normal file
1
frontend/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
15
frontend/src/reportWebVitals.ts
Normal file
15
frontend/src/reportWebVitals.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ReportHandler } from 'web-vitals';
|
||||
|
||||
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||
getCLS(onPerfEntry);
|
||||
getFID(onPerfEntry);
|
||||
getFCP(onPerfEntry);
|
||||
getLCP(onPerfEntry);
|
||||
getTTFB(onPerfEntry);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default reportWebVitals;
|
||||
11
frontend/tailwind.config.js
Normal file
11
frontend/tailwind.config.js
Normal file
@@ -0,0 +1,11 @@
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
Rampart: ["Raleway", "sans-serif"],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
26
frontend/tsconfig.json
Normal file
26
frontend/tsconfig.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
8790
frontend/yarn.lock
Normal file
8790
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
39
hosting/docker-compose.yaml
Normal file
39
hosting/docker-compose.yaml
Normal file
@@ -0,0 +1,39 @@
|
||||
version: "3.3"
|
||||
services:
|
||||
backend:
|
||||
image: fergalmoran/xtream-backend
|
||||
container_name: "xtream-backend"
|
||||
environment:
|
||||
- TZ=Europe/Dublin
|
||||
networks:
|
||||
- "traefik_proxy"
|
||||
restart: always
|
||||
depends_on:
|
||||
- db
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.xtream-backend.rule=Host(`api.streams.fergl.ie`)"
|
||||
- "traefik.http.routers.xtream-backend.entrypoints=websecure"
|
||||
- "traefik.http.routers.xtream-backend.tls.certresolver=noodles-resolver"
|
||||
- "traefik.http.services.xtream-backend.loadbalancer.server.port=80"
|
||||
frontend:
|
||||
image: fergalmoran/xtream-frontend
|
||||
container_name: "xtream-frontend"
|
||||
environment:
|
||||
- TZ=Europe/Dublin
|
||||
networks:
|
||||
- "traefik_proxy"
|
||||
restart: always
|
||||
depends_on:
|
||||
- db
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.xtream-frontend.rule=Host(`streams.fergl.ie`)"
|
||||
- "traefik.http.routers.xtream-frontend.entrypoints=websecure"
|
||||
- "traefik.http.routers.xtream-frontend.tls.certresolver=noodles-resolver"
|
||||
- "traefik.http.services.xtream-frontend.loadbalancer.server.port=80"
|
||||
|
||||
networks:
|
||||
traefik_proxy:
|
||||
external: true
|
||||
name: traefik_proxy
|
||||
2
server/.gitignore
vendored
Normal file
2
server/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.idea/
|
||||
__pycache__/
|
||||
10
server/args.py
Normal file
10
server/args.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import argparse
|
||||
|
||||
|
||||
def get_args():
|
||||
parser = argparse.ArgumentParser(description='Xtream Arguments')
|
||||
|
||||
parser.add_argument('--host', type=str, default='localhost', help='Host address to listen on (default localhost)')
|
||||
parser.add_argument('--port', type=int, default=9531, help='Port to listen on (default 9531)')
|
||||
|
||||
return parser.parse_args()
|
||||
65
server/server.py
Executable file
65
server/server.py
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import urllib.parse as urlparse
|
||||
from http.server import SimpleHTTPRequestHandler, HTTPServer
|
||||
from subprocess import Popen
|
||||
|
||||
from args import get_args
|
||||
|
||||
|
||||
def missing_bin(bin):
|
||||
print("======================")
|
||||
print(
|
||||
f"ERROR: {bin.upper()} does not appear to be installed correctly! please ensure you can launch '{bin}' in the terminal.")
|
||||
print("======================")
|
||||
|
||||
|
||||
class CORSRequestHandler(SimpleHTTPRequestHandler):
|
||||
|
||||
def end_headers(self):
|
||||
self.send_header('Access-Control-Allow-Origin', '*')
|
||||
self.send_header('Access-Control-Allow-Methods', '*')
|
||||
self.send_header('Access-Control-Allow-Headers', '*')
|
||||
self.send_header('Access-Control-Allow-Private-Network', 'true')
|
||||
self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate')
|
||||
return super(CORSRequestHandler, self).end_headers()
|
||||
|
||||
def do_OPTIONS(self):
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
def do_GET(self):
|
||||
try:
|
||||
url = urlparse.urlparse(self.path)
|
||||
query = urlparse.parse_qs(url.query)
|
||||
except:
|
||||
query = {}
|
||||
|
||||
urls = str(query["play_url"][0])
|
||||
if urls.startswith('magnet:') or urls.endswith('.torrent'):
|
||||
try:
|
||||
pipe = Popen([
|
||||
'peerflix', '-k', urls, '--', '--force-window'
|
||||
] + query.get("mpv_args", []))
|
||||
except FileNotFoundError as e:
|
||||
missing_bin('peerflix')
|
||||
else:
|
||||
try:
|
||||
pipe = Popen(['mpv', urls, '--force-window'] +
|
||||
query.get("mpv_args", []))
|
||||
except FileNotFoundError as e:
|
||||
missing_bin('mpv')
|
||||
self.send_response(200, "playing...")
|
||||
|
||||
self.send_response(200)
|
||||
self.end_headers()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = get_args()
|
||||
print(args.host)
|
||||
print(args.port)
|
||||
print("Listening on {}:{}".format(args.host, args.port))
|
||||
|
||||
httpd = HTTPServer((args.host, args.port), CORSRequestHandler)
|
||||
httpd.serve_forever()
|
||||
Reference in New Issue
Block a user