Compare commits

..

6 Commits

Author SHA1 Message Date
Jan Stabenow
03784af8a4
Mod redesigned rtmp + add tmp. fix 2022-07-07 11:34:02 +02:00
Jan Stabenow
cdd149ea33
Mod removes &transtype=live (set by cores placeholder) 2022-07-07 11:32:24 +02:00
Jan Stabenow
cd2613b758
Mod replaces static RTMP and SRT addresses with core placeholders 2022-07-07 10:54:13 +02:00
Ingo Oppermann
13852b81b0
Display copy buttons according to availability 2022-07-06 20:54:23 +02:00
Ingo Oppermann
4293fea917
Provide RTMP and SRT control with availability of the server 2022-07-06 20:44:04 +02:00
Jan Stabenow
4344f0bdc9
Add stream distribution across multiple internal servers 2022-07-06 16:37:41 +02:00
279 changed files with 44059 additions and 87377 deletions

View File

@ -10,4 +10,3 @@ node_modules/
.github .github
.github_build .github_build
.build .build
NONPUBLIC/

View File

@ -1 +0,0 @@
src/locales/*/messages.js

View File

@ -1,88 +1,61 @@
name: 'Build main restreamer-ui' name: 'Build restreamer-ui'
on: on:
workflow_dispatch: workflow_dispatch:
push:
branches-ignore:
- '**'
jobs: jobs:
build-frontend: docker:
runs-on: ubuntu-latest runs-on: [self-hosted]
outputs: steps:
version: ${{ steps.latestversion.outputs.version }} - name: Checkout
steps: uses: actions/checkout@v2
- name: Checkout
uses: actions/checkout@v4
- name: Get latest version from package.json - uses: cardinalby/export-env-action@v1
id: latestversion with:
run: | envFile: '.github_build/Build.restreamer-ui.env'
echo "version=$(cat ./package.json | jq '.version' | sed 's/\"//g')" >> "$GITHUB_OUTPUT" export: 'true'
expandWithJobEnv: 'true'
expand: 'true'
- name: Set up Node.js - name: Set up QEMU
uses: actions/setup-node@v4 uses: docker/setup-qemu-action@master
with: with:
node-version: '21' platforms: all
- name: Build React App - name: Set up Docker Buildx
run: | id: buildx
yarn install uses: docker/setup-buildx-action@master
yarn build
env:
PUBLIC_URL: './'
- name: Upload React build as artifact - name: Cache Docker layers
uses: actions/upload-artifact@v4 uses: actions/cache@v2
with: with:
name: restreamerui-main-build path: /tmp/.buildx-cache
path: build/ key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
build-docker: - name: Login to DockerHub
needs: build-frontend if: github.event_name != 'pull_request'
runs-on: [self-hosted] uses: docker/login-action@v1
steps: with:
- name: Checkout username: ${{ secrets.DOCKER_USERNAME }}
uses: actions/checkout@v4 password: ${{ secrets.DOCKER_PASSWORD }}
- name: Download React build artifact - name: Build Multi-Arch
uses: actions/download-artifact@v4 uses: docker/build-push-action@v2
with: with:
name: restreamerui-main-build builder: ${{ steps.buildx.outputs.name }}
path: build context: .
file: ./Dockerfile
- name: Docker meta build-args: |
id: meta PUBLIC_URL=/ui
uses: docker/metadata-action@v5 platforms: linux/amd64,linux/arm64,linux/arm/v7
with: push: true
images: | tags: |
datarhei/restreamer-ui datarhei/restreamer-ui:${{ env.RELEASE }}
tags: | datarhei/restreamer-ui:latest
type=raw,value=latest cache-from: type=local,src=/tmp/.buildx-cache
type=raw,value=${{ needs.build-frontend.outputs.version }} cache-to: type=local,dest=/tmp/.buildx-cache-new
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile.workflow
build-args: |
CADDY_IMAGE=caddy:2.7.6-alpine
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -1,88 +0,0 @@
name: 'Build dev restreamer-ui'
on:
workflow_dispatch:
workflow_call:
schedule:
- cron: '37 4 * * *'
push:
branches:
- dev
jobs:
build-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: dev
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '21'
- name: Build React App
run: |
yarn install
yarn build
env:
PUBLIC_URL: './'
- name: Upload React build as artifact
uses: actions/upload-artifact@v4
with:
name: restreamerui-dev-build
path: build/
build-docker:
needs: build-frontend
runs-on: [self-hosted]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download React build artifact
uses: actions/download-artifact@v4
with:
name: restreamerui-dev-build
path: build
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
datarhei/restreamer-ui
tags: |
type=raw,value=dev
- name: Set up QEMU
uses: docker/setup-qemu-action@master
with:
platforms: all
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@master
- name: Login to DockerHub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Docker Image
uses: docker/build-push-action@v5
with:
builder: ${{ steps.buildx.outputs.name }}
context: .
file: ./Dockerfile.workflow
build-args: |
CADDY_IMAGE=caddy:2.7.6-alpine
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

View File

@ -0,0 +1,2 @@
# RESTREAMER UI
RELEASE=1.1.0

2
.gitignore vendored
View File

@ -1,7 +1,6 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies # dependencies
/NONPUBLIC
/node_modules /node_modules
/.pnp /.pnp
.pnp.js .pnp.js
@ -13,7 +12,6 @@
/build /build
# misc # misc
NONPUBLIC
.DS_Store .DS_Store
.VSCodeCounter .VSCodeCounter
.env.local .env.local

View File

@ -14,19 +14,12 @@
"sourceLocale": "en", "sourceLocale": "en",
"locales": [ "locales": [
"en", "en",
"da",
"de", "de",
"el",
"es",
"fr", "fr",
"it", "it",
"ko",
"pl", "pl",
"pt-br", "pt",
"ru", "es",
"sl", "ru"
"tr",
"uk",
"zh-hans"
] ]
} }

View File

@ -1,201 +1,19 @@
# Restreamer-UI # Restreamer-UI
## v1.13.0 > v1.14.0 #### v1.1.0 > v1.2.0
- Add wettercom service
- Add option to select which channels will be displayed on the playersite ([#392](https://github.com/datarhei/restreamer/issues/392), [#800](https://github.com/datarhei/restreamer/issues/800))
- Mod updates public videojs >v8
- Fix erroneous filter setting
- Fix encoded address
- Fix double -filter parameter when encoder sets filter
- Fix Docker build ([#64](https://github.com/datarhei/restreamer-ui/issues/64))
## v1.12.0 > v1.13.0
- Add to allow stream hints in case probing fails
- Mod enables ff-loglevel and prepares the logging component
- Mod uses official Instagram-RTMP target
- Mod Remove unused imports
- Mod Update translations
- Mod updates dep.
- Fix player position
- Fix missing stream URL, summarize streams in probe log, don't lock type for first stream
## v1.11.0 > v1.12.0
- Add option to select different SRT stream in wizard
- Add option to select different RTMP stream in wizard
- Fix selecting other than first audio stream ([#710](https://github.com/datarhei/restreamer/issues/710))
- Fix reset of previous audio settings when editing profile ([#730](https://github.com/datarhei/restreamer/issues/730))
- Fix RTMP URL for receive mode
## v1.10.0 > v1.11.0
- Add allow to stream HEVC and AV1 to Youtube via RTMP
- Add librav1e AV1 encoder
- Add support for AV1 CUDA decoding ([PR 46](https://github.com/datarhei/restreamer-ui/pull/46))
- Add FFmpeg 6 support
- Add HEVC VideoToolbox encoder
- Fix anonymize error message ([#688](https://github.com/datarhei/restreamer/issues/688))
- Fix chromecast config ([#37](https://github.com/datarhei/restreamer-ui/issues/37))
## v1.9.0 > v1.10.0
- Add resource usage and ffmpeg command to process details
- Add audio loop source
- Add to allow to select from already publishing RTMP and SRT streams
- Fix wrongly displayed SRT URL ([#635](https://github.com/datarhei/restreamer/issues/635))
- Fix RTMPS address with custom ports ([#658](https://github.com/datarhei/restreamer/issues/658))
- Fix allow RTSPS protocol ([#677](https://github.com/datarhei/restreamer/issues/677))
## v1.8.0 > v1.9.0
- Add enlarged channel overview
- Add new publication services: Dailymotion, Livepush, kick.com, NimoTV, PicartoTV, Rumble
- Add frame interpolation (framerate) filter (thanks to orryverducci)
- Add -referer option for pulling HTTP streams ([PR 40](https://github.com/datarhei/restreamer-ui/pull/40), thanks to mdastgheib)
- Add a/v filter to the publication components ([#593](https://github.com/datarhei/restreamer-ui/issues/593))
- Add video or image loop as input ([#528](https://github.com/datarhei/restreamer/discussions/528))
- Add option for custom poster image in player ([#632](https://github.com/datarhei/restreamer/issues/632))
- Add option to allow to set limits for ingest and egress processes ([#636](https://github.com/datarhei/restreamer/issues/636))
- Mod extends twitch's server list
- Mod uses placeholders for ingress setups ([#560](https://github.com/datarhei/restreamer-ui/issues/560))
- Mod updates npm
- Fix Owncast typo
- Fix Restream grid
- Fix the advanced settings in the MPEG-TS publication service ([#597](https://github.com/datarhei/restreamer/issues/597), thanks to orryverducci)
- Fix ALSA demuxer option names
- Fix index out-of-range warning, list ALSA devices for Raspicam video source
- Fix MUI warning
- Fix videojs skin
## v1.7.0 > v1.8.0
- Add stream key field and protocol detection to RTMP publication service
- Add Chinese (simplified) translation (thanks to Huyg0180110559)
- Add Ukrainian translation (thanks to Yurii Denys)
- Fix empty force_key_frames value
- Fix Icecast publication service
- Fix imprint, terms and credit without share ([#525](https://github.com/datarhei/restreamer/issues/529))
- Fix proxy error on the playersite ([#525](https://github.com/datarhei/restreamer/issues/525))
- Fix saving RTMP advanced options ([#518](https://github.com/datarhei/restreamer/issues/518))
- Fix help buttons for other languages than English and German ([#24](https://github.com/datarhei/restreamer-ui/issues/24))
- Fix internal player skin (volume bar)
- Fix security hints (npm dep.)
## v1.6.0 > v1.7.0
- Add analyzeduration, probesize and max_probe_packets input options
- Add avoid_negative_ts input option
- Add http_proxy input option ([#513](https://github.com/datarhei/restreamer/issues/513))
- Add copyts, start_at_zero and use_wallclock_as_timestamps input options
- Add heuristic to find core address if UI is proxied
- Add Turkish translation (thanks to Ramazan Sancar) ([#22](https://github.com/datarhei/restreamer-ui/issues/22))
- Add Danish translation (Thanks to Filip Stadler and Info)
- Add Slovenian translation (thanks to Grega)
- Add Greek translation
- Mod allows general input settings for pull and push streams
- Mod updates npm dependencies
- Fix Creative Commons icons
- Fix positioning of the deinterlacing filter ([#465](https://github.com/datarhei/restreamer/issues/465))
## v1.5.1 > v1.6.0
- Add Bob Weaver Deinterlacing Filter ([#465](https://github.com/datarhei/restreamer/issues/465))
- Add tests for wizard, network source, and coders
- Add Korean translation (thanks to Jihaeng)
- Mod splitting wizard in components
- Fix wrong call to encoder defaults ([#467](https://github.com/datarhei/restreamer/issues/467))
## v1.5.0 > v1.5.1
- Fix FFmpeg version check for RTSP sources ([#455](https://github.com/datarhei/restreamer/issues/455))
- Fix requires Core >= v16.11.0 and FFmpeg >= 5.1.0
## v1.4.0 > v1.5.0
- Add changelog viewer
- Add skills props to encoder and decoder components
- Add fps_mode to x264, x265, vp9 encoder
- Add scale filter to non-hwaccel encoders
- Add PeerTube and Media Network to publication services (plattforms, software)
- Add reset button to hide a player logo ([#431](https://github.com/datarhei/restreamer/issues/431))
- Mod expands V4L2_M2M options (an unstable RPI 64bit encoder)
- Mod indicates a faulty cache configuration
- Mod switches to the improved SRT syntax (thx to SA Consulting)
- Mod improves display of progress data
- Mod removes deprecated param ocl - now ochl (ff5)
- Mod simplifies the setup of Restreamer-to-Restreamer connections
- Mod adds Istafeed.me as StreamKey service to Instagram's publishing service
- Mod renames "Low delay" to "Low latency (buffer)" and set false as default (requires more feedback)
- Del removes support for clappr player
- Fix npm dependencies (security fixes)
- Fix videojs-overlay logo size ([#431](https://github.com/datarhei/restreamer/issues/431))
- Fix use of TLS for input from local RTMP server
- Fix Icecast publication service settings (datarhei/restreamer#429)
- Fix removes SRT bitstream on tee (OBS > RTMP > SRT is faulty)
Dependency:
- datarhei Core v16.11.0+
## v1.3.0 > v1.4.0
- Add email field for Let's Encrypt certification
Dependency:
- datarhei Core v16.10.1+
## v1.2.0 > v1.3.0
- Add dlive & Trovo publication services
- Add low_delay option to processing (default: true)
- Mod uses the ingest stream for publication (datarhei/restreamer#411)
- Mod optimized DVR on DiskFS
- Mod updates packages
- Fix SRT bitstream on tee
- Fix typo
- Fix viewer count (datarhei/restreamer#394)
- Fix user registration if username and/or password are set via environment (datarhei/restreamer-ui#13)
- Fix Dockerfile, Reduce size, serve production build (datarhei/restreamer-ui#12)
Dependency:
- datarhei Core v16.10.0+
## v1.1.0 > v1.2.0
- Add allow writing HLS to disk
- Add audio pan filter
- Add video rotation filter ([#347](https://github.com/datarhei/restreamer/discussions/347))
- Add video h/v flip filter
- Add audio volume filter ([#313](https://github.com/datarhei/restreamer/issues/313))
- Add audio loudness normalization filter
- Add audio resample filter, that was before part of the encoders
- Add HLS Master playlist (requires FFmpeg hlsbitrate.patch) (thx Dwaynarang, Electra Player compatibility)
- Add LinkedIn & Azure Media Services to publication services (thx kalashnikov)
- Add AirPlay support with silvermine videojs plugin
- Add Chromecast support (thx badincite, [#10](https://github.com/datarhei/restreamer-ui/pull/10))
- Add stream distribution across multiple internal servers
- Add SRT settings - Add SRT settings
- Add HLS version selection (thx Dwaynarang, Electra Player compatibility) - Add HLS version selection (Dwaynarang, Electra Player compatibility)
- Add Owncast to publication services ([#369](https://github.com/datarhei/restreamer/issues/369)) - Add Owncast to publication services ([#369](https://github.com/datarhei/restreamer/issues/369))
- Add Telegram to publication services (thx Martin Held) - Add Telegram to publication services (thx Martin Held)
- Add Polish translations (thx Robert Rykała) - Add Polish translations (thx Robert Rykała)
- Mod extends the datarhei Core publication service with srt streaming - Mod Allow decoders and encoders to set global options
- Mod allow decoders and encoders to set global options
- Mod allow trailing slash on Core address
- Fix player problem with different stream formats (9:16)
- Fix process report naming - Fix process report naming
- Fix publication service icon styles - Fix publication service icon styles
- Fix VAAPI encoder - Fix VAAPI encoder
- Allow trailing slash on Core address
Dependency: #### v1.0.0 > v1.1.0
- datarhei Core v16.9.0+
## v1.0.0 > v1.1.0
- Add compatibility list for encoders - Add compatibility list for encoders
- Add "HLS cleanup" as an optional function ([Philipp Trenz](https://github.com/philipptrenz)) - Add "HLS cleanup" as an optional function ([Philipp Trenz](https://github.com/philipptrenz))
@ -204,9 +22,8 @@ Dependency:
- Add missed VAAPI encoder - Add missed VAAPI encoder
- Add missed V4L2_M2M encoder - Add missed V4L2_M2M encoder
- Add missed Raspberry Pi 64bit Docker image - Add missed Raspberry Pi 64bit Docker image
- Add option to disable playersites share-button (thx Anders Mellgren)
- Add security pr
- Mod updates VideoJS - Mod updates VideoJS
- Add option to disable playersites share-button (thx Anders Mellgren)
- Fix hides unset content license on playersite (thx Anders Mellgren) - Fix hides unset content license on playersite (thx Anders Mellgren)
- Fix updates V4L2 device-list on select - Fix updates V4L2 device-list on select
- Fix snapshot interval ([#341](https://github.com/datarhei/restreamer/issues/340)) - Fix snapshot interval ([#341](https://github.com/datarhei/restreamer/issues/340))
@ -217,6 +34,7 @@ Dependency:
- Fix datarhei Core publication service - Fix datarhei Core publication service
- Fix dependabot alerts - Fix dependabot alerts
- Fix code scanning alerts - Fix code scanning alerts
- Merge security pr
Preparation for FFmpeg v5.0 (migration will not work) Preparation for FFmpeg v5.0 (migration will not work)

View File

@ -1,6 +0,0 @@
:3000
encode zstd gzip
file_server {
root ./build
}

View File

@ -1,25 +1,23 @@
ARG NODE_IMAGE=node:21-alpine3.20 FROM node:17.9.0-alpine3.15
ARG CADDY_IMAGE=caddy:2.8.4-alpine
FROM $NODE_IMAGE AS builder ARG NODE_SPACE_SIZE=10240
ENV NODE_OPTIONS="--openssl-legacy-provider --max-old-space-size=$NODE_SPACE_SIZE"
ENV PUBLIC_URL="./" ARG PUBLIC_URL "/"
COPY . /ui COPY . /ui
WORKDIR /ui WORKDIR /ui
RUN cd /ui && \ RUN cd /ui && \
yarn install && \ npm config set fetch-retries 10 && \
yarn build npm config set fetch-retry-mintimeout 100000 && \
npm config set fetch-retry-maxtimeout 600000 && \
FROM $CADDY_IMAGE npm config set cache-min 3600 && \
npm config ls -l && \
COPY --from=builder /ui/build /ui/build npm install && \
COPY --from=builder /ui/Caddyfile /ui/Caddyfile npm run build
WORKDIR /ui
EXPOSE 3000 EXPOSE 3000
CMD [ "caddy", "run", "--config", "/ui/Caddyfile" ] CMD [ "npm", "run", "start" ]

View File

@ -1,13 +0,0 @@
ARG CADDY_IMAGE=caddy:2.7.5-alpine
FROM $CADDY_IMAGE
COPY build /ui/build
COPY Caddyfile /ui/Caddyfile
ENV PUBLIC_URL="./"
WORKDIR /ui
EXPOSE 3000
CMD [ "caddy", "run", "--config", "/ui/Caddyfile" ]

View File

@ -1,6 +1,6 @@
# Restreamer-UI # Restreamer-UI
The user interface of the Restreamer for the connection to the [datarhei Core](https://github.com/datarhei/core)application. The user interface of the Restreamer for the connection to the Core application.
- React - React
- Material-UI (MUI) - Material-UI (MUI)
@ -17,7 +17,7 @@ $ npm run start
``` ```
Connect the UI with a [datarhei Core](https://github.com/datarhei/core): Connect the UI with a [datarhei Core](https://github.com/datarhei/core):
http://localhost:3000?address=http://core-ip:core-port http://localhost:3000?address=http://core-ip:core-port/
### To add/fix translations: ### To add/fix translations:
Locales are located in `src/locals` Locales are located in `src/locals`

View File

@ -1,61 +1,57 @@
{ {
"name": "restreamer-ui", "name": "restreamer-ui",
"version": "1.14.0", "version": "1.2.0",
"bundle": "restreamer-v2.12.0", "bundle": "restreamer-v2.x.x",
"private": false, "private": false,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@auth0/auth0-spa-js": "^2.1.3", "@auth0/auth0-spa-js": "^1.22.0",
"@babel/plugin-syntax-flow": "^7.24.7", "@clappr/core": "^0.4.21",
"@babel/plugin-transform-react-jsx": "^7.25.2", "@clappr/hlsjs-playback": "^0.6.0",
"@emotion/react": "^11.13.3", "@clappr/plugins": "^0.4.16",
"@emotion/styled": "^11.13.0", "@clappr/stats-plugin": "^0.2.0",
"@fontsource/dosis": "^5.0.21", "@emotion/react": "^11.9.0",
"@fontsource/roboto": "^5.0.14", "@emotion/styled": "^11.8.1",
"@fortawesome/fontawesome-svg-core": "^6.6.0", "@fontsource/dosis": "^4.5.8",
"@fortawesome/free-brands-svg-icons": "^6.6.0", "@fontsource/roboto": "^4.5.7",
"@fortawesome/free-solid-svg-icons": "^6.6.0", "@fortawesome/fontawesome-svg-core": "^6.1.1",
"@fortawesome/react-fontawesome": "^0.2.2", "@fortawesome/free-brands-svg-icons": "^6.1.1",
"@lingui/core": "^4.11.4", "@fortawesome/free-solid-svg-icons": "^6.1.1",
"@lingui/macro": "^4.11.4", "@fortawesome/react-fontawesome": "^0.1.18",
"@lingui/react": "^4.11.4", "@lingui/core": "^3.13.3",
"@mui/icons-material": "^6.0.1", "@lingui/macro": "^3.13.3",
"@mui/lab": "^6.0.0-beta.8", "@lingui/react": "^3.13.3",
"@mui/material": "^6.0.1", "@mui/icons-material": "^5.8.2",
"@mui/styles": "^6.0.1", "@mui/lab": "^5.0.0-alpha.84",
"@testing-library/dom": "^10.4.0", "@mui/material": "5.1.1",
"@testing-library/jest-dom": "^6.5.0", "@mui/styles": "^5.1.1",
"@testing-library/react": "^16.0.1", "@testing-library/dom": "^8.13.0",
"@testing-library/user-event": "^14.5.2", "@testing-library/jest-dom": "^4.2.4",
"@types/react": "^18.3.5", "@testing-library/react": "^12.1.5",
"@testing-library/user-event": "^13.5.0",
"babel-plugin-macros": "^3.1.0", "babel-plugin-macros": "^3.1.0",
"eslint": "^9.9.1", "eslint": "^7.32.0",
"handlebars": "^4.7.8", "handlebars": "^4.7.7",
"jwt-decode": "^4.0.0", "hls.js": "^0.14.17",
"make-plural": "^7.4.0", "jwt-decode": "^3.1.2",
"react": "^18.3.1", "make-plural": "^7.1.0",
"react-colorful": "^5.6.1", "react": "^17.0.2",
"react-device-detect": "^2.2.3", "react-colorful": "^5.5.1",
"react-dom": "^18.3.1", "react-device-detect": "^2.2.2",
"react-markdown": "^9.0.1", "react-dom": "^17.0.2",
"react-router-dom": "^6.26.1", "react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1", "react-scripts": "^4.0.3",
"semver": "^7.6.3", "semver": "^7.3.7",
"serve": "^14.2.3", "typescript": "^3.9.7",
"typescript": "^5.5.4",
"url-parse": "^1.5.10", "url-parse": "^1.5.10",
"util": "^0.12.5", "uuid": "^8.3.2",
"uuid": "^10.0.0", "video.js": "^7.19.2",
"video.js": "^8.17.3", "videojs-overlay": "^2.1.5"
"videojs-overlay": "^3.1.0"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts --optimize-for-size build", "build": "react-scripts --optimize-for-size build",
"start-build": "serve -s build",
"test": "react-scripts test", "test": "react-scripts test",
"test-ci": "react-scripts test --watchAll=false --testTimeout 50000",
"test-coverage": "react-scripts test --watchAll=false --testTimeout 50000 --coverage",
"eject": "react-scripts eject", "eject": "react-scripts eject",
"i18n-extract": "lingui extract", "i18n-extract": "lingui extract",
"i18n-extract:clean": "lingui extract --clean", "i18n-extract:clean": "lingui extract --clean",
@ -77,7 +73,7 @@
}, },
"browserslist": { "browserslist": {
"production": [ "production": [
"> 0.5%, last 2 versions, Firefox ESR, not dead, not IE 11" "> 0.5%, last 2 versions, Firefox ESR, not dead, not IE 11, maintained node versions"
], ],
"development": [ "development": [
"last 1 chrome version", "last 1 chrome version",
@ -86,13 +82,14 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.18.2",
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@lingui/cli": "^3.13.3",
"@lingui/cli": "^4.11.4", "babel-core": "^7.0.0-bridge.0",
"babel-core": "^6.26.3", "prettier": "^2.6.2",
"eslint-config-react-app": "^7.0.1",
"prettier": "^3.3.3",
"react-error-overlay": "^6.0.11" "react-error-overlay": "^6.0.11"
}, },
"resolutions": {} "resolutions": {
"url-parse@1.5.3": "patch:url-parse@npm:1.5.3#.yarn/patches/url-parse-npm-1.5.3-225ab9cae7.patch",
"react-error-overlay": "6.0.9"
}
} }

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,4 @@
dist/clappr.min.js.map
dist/clappr.min.js
dist/clappr-stats.min.js
dist/clappr-nerd-stats.min.js

View File

@ -0,0 +1,112 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="{{description}}">
<meta name="author" content="datarhei restreamer">
<title>{{name}}</title>
<link rel="shortcut icon" type="image/x-icon" href="favicon.ico">
<link rel="alternate" type="application/json+oembed" href="channels/{{channelid}}/oembed.json" title="{{name}}">
<link rel="alternate" type="text/xml+oembed" href="channels/{{channelid}}/oembed.xml" title="{{name}}">
<script src="channels/{{channelid}}/config.js"></script>
<script src="player/clappr/dist/clappr.min.js"></script>
<script src="player/clappr/dist/clappr-stats.min.js"></script>
<script src="player/clappr/dist/clappr-nerd-stats.min.js"></script>
<style>
.player-poster[data-poster] .poster-background[data-poster] {
height: initial !important;
}
</style>
</head>
<body>
<div id="player" style="position:absolute;top:0;right:0;bottom:0;left:0"></div>
<script>
function getQueryParam(key, defaultValue) {
var query = window.location.search.substring(1);
var vars = query.split("&");
for(var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if(pair[0] == key) {
return pair[1];
}
}
return defaultValue;
}
function convertBoolParam(key, defaultValue) {
var val = getQueryParam(key, defaultValue);
return val === true || val === "true" || val === "1" || val === "yes" || val === "on";
}
function convertColorParam(parameter, defaultColor) {
var re = new RegExp("^#([0-9a-f]{3}|[0-9a-f]{6})$");
var c = getQueryParam(parameter, defaultColor);
// decode color as # has to be represented by %23
var c = decodeURIComponent(c);
// if color was given without leading #, prepend it
if (!String(c).startsWith("#")) c = "#" + c;
if (re.test(c)) {
return c;
} else {
return defaultColor;
}
}
var autoplay = convertBoolParam("autoplay", playerConfig.autoplay);
var mute = convertBoolParam("mute", playerConfig.mute);
var statistics = convertBoolParam("stats", playerConfig.statistics);
var color = convertColorParam("color", playerConfig.color.buttons);
var plugins = [];
if(statistics == true) {
plugins.push(ClapprNerdStats);
plugins.push(ClapprStats);
}
var config = {
source: playerConfig.source,
parentId: '#player',
baseUrl: 'clappr/',
plugins: plugins,
poster: playerConfig.poster + '?t=' + String(new Date().getTime()),
mediacontrol: {
seekbar: playerConfig.color.seekbar,
buttons: color
},
height: '100%',
width: '100%',
disableCanAutoPlay: true,
autoPlay: autoplay,
mute: mute,
clapprStats: {
runEach: 1000,
onReport: (metrics) => {},
},
clapprNerdStats: {
shortcut: ['command+shift+s', 'ctrl+shift+s'],
iconPosition: 'top-right'
}
};
if(playerConfig.logo.image.length != 0) {
config.watermark = playerConfig.logo.image;
config.position = playerConfig.logo.position;
if(playerConfig.logo.link.length != 0) {
config.watermarkLink = playerConfig.logo.link;
}
}
var player = new window.Clappr.Player(config);
var posterPlugin = player.core.mediaControl.container.getPlugin('poster');
player.on(window.Clappr.Events.PLAYER_STOP, function updatePoster () {
posterPlugin.options.poster = playerConfig.poster + '?t=' + String(new Date().getTime());
posterPlugin.render();
});
</script>
</body>
</html>

View File

@ -1,15 +0,0 @@
<svg fill="#FFFFFF" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path d="M0 0h24v24H0V0z" id="a"/>
</defs>
<defs>
<path d="M0 0h24v24H0V0z" id="c"/>
</defs>
<clipPath id="b">
<use overflow="visible" xlink:href="#a"/>
</clipPath>
<clipPath clip-path="url(#b)" id="d">
<use overflow="visible" xlink:href="#c"/>
</clipPath>
<path clip-path="url(#d)" d="M6 22h12l-6-6zM21 3H3c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h4v-2H3V5h18v12h-4v2h4c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 981 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

View File

@ -95,7 +95,7 @@
/* volume-panel */ /* volume-panel */
.vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal { .vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
padding-top: .9em; padding-top: 1em;
} }
.vjs-public .vjs-control .vjs-volume-panel { .vjs-public .vjs-control .vjs-volume-panel {
@ -110,8 +110,8 @@
/* disable caps */ /* disable caps */
.vjs-public .vjs-subs-caps-button { .vjs-internal .vjs-subs-caps-button {
display: none!important; display: none;
} }
/* spacer */ /* spacer */
@ -123,37 +123,33 @@
/* overlay */ /* overlay */
.vjs-public .vjs-overlay-no-background { .vjs-public .vjs-overlay > a > img {
max-width: 25%!important; width: 100%;
} }
.vjs-public .vjs-overlay-no-background > img, .vjs-public .vjs-overlay-no-background > a > img { .vjs-public .vjs-overlay-no-background {
max-width: 100%!important; max-width: 28%!important;
height: auto!important; max-height: 28%!important;
} }
.vjs-public .vjs-overlay-top-left { .vjs-public .vjs-overlay-top-left {
top: 15px!important; top: 20px!important;
left: 20px!important; left: 30px!important;
text-align: right;
} }
.vjs-public .vjs-overlay-top-right { .vjs-public .vjs-overlay-top-right {
top: 15px!important; top: 20px!important;
right: 20px!important; right: 30px!important;
text-align: right;
} }
.vjs-public .vjs-overlay-bottom-left { .vjs-public .vjs-overlay-bottom-left {
bottom: 15px!important; bottom: 20px!important;
left: 20px!important; left: 30px!important;
text-align: left;
} }
.vjs-public .vjs-overlay-bottom-right { .vjs-public .vjs-overlay-bottom-right {
bottom: 15px!important; bottom: 20px!important;
right: 20px!important; right: 30px!important;
text-align: left;
} }
/* context menu */ /* context menu */
@ -170,3 +166,4 @@
.vjs-public .vjs-lock-open { .vjs-public .vjs-lock-open {
z-index: 1000; z-index: 1000;
} }

View File

@ -1 +1 @@
.vjs-public{--video-js--primary:#EAEA05}.vjs-public .vjs-big-play-button{width:70px;height:70px;background:none;line-height:180px;font-size:180px;border:0;top:50%;left:50%;margin-top:-90px;margin-left:-90px;color:rgba(255,255,255,.65)}.vjs-public:hover .vjs-big-play-button,.vjs-public.vjs-big-play-button:focus{background-color:transparent;color:rgba(255,255,255,1)}.vjs-public .vjs-control-bar{height:70px;padding-top:20px;background:none;background-image:linear-gradient(0,rgba(0,0,0,.85),transparent)}.vjs-public .vjs-time-tooltip{z-index:0}.vjs-public .vjs-button>.vjs-icon-placeholder:before{line-height:50px}.vjs-public .vjs-play-progress:before{display:none}.vjs-public .vjs-progress-control{position:absolute;top:0;right:0;left:15px;width:calc(100% - 30px);height:20px}.vjs-public .vjs-progress-control .vjs-progress-holder{position:absolute;top:20px;right:0;left:0;width:100%;margin:0}.vjs-public .vjs-play-progress{background-color:var(--video-js--primary)}.vjs-public .vjs-slider{background:rgba(255,255,255,.25)}.vjs-public .vjs-load-progress{background:rgba(255,255,255,.25)}.vjs-public .vjs-load-progress div{background:rgba(255,255,255,.25)}.vjs-public .vjs-remaining-time{order:0;line-height:50px;flex:3;text-align:left}.vjs-public .vjs-live-control{line-height:50px}.vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{padding-top:.9em}.vjs-public .vjs-control .vjs-volume-panel{width:4.5em}.vjs-public .vjs-live-display{margin-left:1.8em}.vjs-public .vjs-subs-caps-button{display:none!important}.vjs-public .vjs-custom-control-spacer{display:block;width:100%}.vjs-public .vjs-overlay-no-background{max-width:25%!important}.vjs-public .vjs-overlay-no-background>img,.vjs-public .vjs-overlay-no-background>a>img{max-width:100%!important;height:auto!important}.vjs-public .vjs-overlay-top-left{top:15px!important;left:20px!important;text-align:right}.vjs-public .vjs-overlay-top-right{top:15px!important;right:20px!important;text-align:right}.vjs-public .vjs-overlay-bottom-left{bottom:15px!important;left:20px!important;text-align:left}.vjs-public .vjs-overlay-bottom-right{bottom:15px!important;right:20px!important;text-align:left}.vjs-public .vjs-license .vjs-menu .vjs-menu-content{background:rgba(0,0,0,.8)}.vjs-public .vjs-license-top-level-header{background:unset!important;border-bottom:1px solid rgba(255,255,255,.25)}.vjs-public .vjs-lock-open{z-index:1000} .vjs-public{--video-js--primary:#EAEA05}.vjs-public .vjs-big-play-button{width:70px;height:70px;background:0 0;line-height:180px;font-size:180px;border:none;top:50%;left:50%;margin-top:-90px;margin-left:-90px;color:rgba(255,255,255,.65)}.vjs-public.vjs-big-play-button:focus,.vjs-public:hover .vjs-big-play-button{background-color:transparent;color:rgba(255,255,255,1)}.vjs-public .vjs-control-bar{height:70px;padding-top:20px;background:0 0;background-image:linear-gradient(0deg,rgba(0,0,0,.85),transparent)}.vjs-public .vjs-time-tooltip{z-index:0}.vjs-public .vjs-button>.vjs-icon-placeholder:before{line-height:50px}.vjs-public .vjs-play-progress:before{display:none}.vjs-public .vjs-progress-control{position:absolute;top:0;right:0;left:15px;width:calc(100% - 30px);height:20px}.vjs-public .vjs-progress-control .vjs-progress-holder{position:absolute;top:20px;right:0;left:0;width:100%;margin:0}.vjs-public .vjs-play-progress{background-color:var(--video-js--primary)}.vjs-public .vjs-slider{background:rgba(255,255,255,.25)}.vjs-public .vjs-load-progress{background:rgba(255,255,255,.25)}.vjs-public .vjs-load-progress div{background:rgba(255,255,255,.25)}.vjs-public .vjs-remaining-time{order:0;line-height:50px;flex:3;text-align:left}.vjs-public .vjs-live-control{line-height:50px}.vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{padding-top:1em}.vjs-public .vjs-control .vjs-volume-panel{width:4.5em}.vjs-public .vjs-live-display{margin-left:1.8em}.vjs-internal .vjs-subs-caps-button{display:none}.vjs-public .vjs-custom-control-spacer{display:block;width:100%}.vjs-public .vjs-overlay>a>img{width:100%}.vjs-public .vjs-overlay-no-background{max-width:28%!important;max-height:28%!important}.vjs-public .vjs-overlay-top-left{top:20px!important;left:30px!important}.vjs-public .vjs-overlay-top-right{top:20px!important;right:30px!important}.vjs-public .vjs-overlay-bottom-left{bottom:20px!important;left:30px!important}.vjs-public .vjs-overlay-bottom-right{bottom:20px!important;right:30px!important}.vjs-public .vjs-license .vjs-menu .vjs-menu-content{background:rgba(0,0,0,.8)}.vjs-public .vjs-license-top-level-header{background:unset!important;border-bottom:1px solid rgba(255,255,255,.25)}.vjs-public .vjs-lock-open{z-index:1000}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.vjs-airplay-button .vjs-icon-placeholder{background:url("ic_airplay_white_24px.svg") center center no-repeat;background-size:contain;display:inline-block;width:12px;height:12px}.vjs-airplay-button:hover{cursor:pointer}.vjs-airplay-button:hover .vjs-icon-placeholder{background-image:url("ic_airplay_white_24px.svg")}.vjs-airplay-button.vjs-airplay-button-lg:not(.vjs-hidden){display:flex;align-items:center;width:auto;padding:0 4px}.vjs-airplay-button.vjs-airplay-button-lg:not(.vjs-hidden) .vjs-airplay-button-label{flex-grow:1;margin-left:4px}.vjs-airplay-button.vjs-airplay-button-lg:not(.vjs-hidden) .vjs-icon-placeholder{flex-grow:1}

View File

@ -1,308 +0,0 @@
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
"use strict";
var hasAirPlayAPISupport = require('../lib/hasAirPlayAPISupport');
/**
* Registers the AirPlayButton Component with Video.js. Calls
* {@link http://docs.videojs.com/Component.html#.registerComponent}, which will add a
* component called `airPlayButton` to the list of globally registered Video.js
* components. The `airPlayButton` is added to the player's control bar UI automatically
* once {@link module:enableAirPlay} has been called. If you would like to specify the
* order of the buttons that appear in the control bar, including this button, you can do
* so in the options that you pass to the `videojs` function when creating a player:
*
* ```
* videojs('playerID', {
* controlBar: {
* children: [
* 'playToggle',
* 'progressControl',
* 'volumePanel',
* 'fullscreenToggle',
* 'airPlayButton',
* ],
* }
* });
* ```
*
* @param videojs {object} A reference to {@link http://docs.videojs.com/module-videojs.html|Video.js}
* @see http://docs.videojs.com/module-videojs.html#~registerPlugin
*/
module.exports = function (videojs) {
/**
* The AirPlayButton module contains both the AirPlayButton class definition and the
* function used to register the button as a Video.js Component.
*
* @module AirPlayButton
*/
const ButtonComponent = videojs.getComponent('Button');
/**
* The Video.js Button class is the base class for UI button components.
*
* @external Button
* @see {@link http://docs.videojs.com/Button.html|Button}
*/
/** @lends AirPlayButton.prototype */
class AirPlayButton extends ButtonComponent {
/**
* This class is a button component designed to be displayed in the
* player UI's control bar. It displays an Apple AirPlay selection
* list when clicked.
*
* @constructs
* @extends external:Button
*/
constructor(player, options) {
super(player, options);
if (!hasAirPlayAPISupport()) {
this.hide();
}
this._reactToAirPlayAvailableEvents();
if (options.addAirPlayLabelToButton) {
this.el().classList.add('vjs-airplay-button-lg');
this._labelEl = document.createElement('span');
this._labelEl.classList.add('vjs-airplay-button-label');
this._labelEl.textContent = this.localize('AirPlay');
this.el().appendChild(this._labelEl);
} else {
this.controlText('Start AirPlay');
}
}
/**
* Overrides Button#buildCSSClass to return the classes used on the button element.
*
* @param {DOMElement} el
* @see {@link http://docs.videojs.com/Button.html#buildCSSClass|Button#buildCSSClass}
*/
buildCSSClass() {
return 'vjs-airplay-button ' + super.buildCSSClass();
}
/**
* Overrides Button#handleClick to handle button click events. AirPlay
* functionality is handled outside of this class, which should be limited
* to UI related logic. This function simply triggers an event on the player.
*
* @fires AirPlayButton#airPlayRequested
* @param {DOMElement} el
* @see {@link http://docs.videojs.com/Button.html#handleClick|Button#handleClick}
*/
handleClick() {
this.player().trigger('airPlayRequested');
}
/**
* Gets the underlying DOMElement used by the player.
*
* @private
* @returns {DOMElement} either an <audio> or <video> tag, depending on the type of
* player
*/
_getMediaEl() {
var playerEl = this.player().el();
return playerEl.querySelector('video, audio');
}
/**
* Binds a listener to the `webkitplaybacktargetavailabilitychanged` event, if it is
* supported, that will show or hide this button Component based on the availability
* of the AirPlay function.
*
* @private
*/
_reactToAirPlayAvailableEvents() {
var mediaEl = this._getMediaEl(),
self = this;
if (!mediaEl || !hasAirPlayAPISupport()) {
return;
}
function onTargetAvailabilityChanged(event) {
if (event.availability === 'available') {
self.show();
} else {
self.hide();
}
}
mediaEl.addEventListener('webkitplaybacktargetavailabilitychanged', onTargetAvailabilityChanged);
this.on('dispose', function () {
mediaEl.removeEventListener('webkitplaybacktargetavailabilitychanged', onTargetAvailabilityChanged);
});
}
}
videojs.registerComponent('airPlayButton', AirPlayButton);
};
},{"../lib/hasAirPlayAPISupport":4}],2:[function(require,module,exports){
"use strict";
/**
* @module enableAirPlay
*/
var hasAirPlayAPISupport = require('./lib/hasAirPlayAPISupport');
/**
* @private
* @param {object} the Video.js Player instance
* @returns {AirPlayButton} or `undefined` if it does not exist
*/
function getExistingAirPlayButton(player) {
return player.controlBar.getChild('airPlayButton');
}
/**
* Adds the AirPlayButton Component to the player's ControlBar component, if the
* AirPlayButton does not already exist in the ControlBar.
* @private
* @param player {object} the Video.js Player instance
* @param options {object}
*/
function ensureAirPlayButtonExists(player, options) {
var existingAirPlayButton = getExistingAirPlayButton(player),
indexOpt;
if (options.addButtonToControlBar && !existingAirPlayButton) {
// Figure out AirPlay button's index
indexOpt = player.controlBar.children().length;
if (typeof options.buttonPositionIndex !== 'undefined') {
indexOpt = options.buttonPositionIndex >= 0 ? options.buttonPositionIndex : player.controlBar.children().length + options.buttonPositionIndex;
}
player.controlBar.addChild('airPlayButton', options, indexOpt);
}
}
/**
* Handles requests for AirPlay triggered by the AirPlayButton Component.
*
* @private
* @param player {object} the Video.js Player instance
*/
function onAirPlayRequested(player) {
var mediaEl = player.el().querySelector('video, audio');
if (mediaEl && mediaEl.webkitShowPlaybackTargetPicker) {
mediaEl.webkitShowPlaybackTargetPicker();
}
}
/**
* Adds an event listener for the `airPlayRequested` event triggered by the AirPlayButton
* Component.
*
* @private
* @param player {object} the Video.js Player instance
*/
function listenForAirPlayEvents(player) {
// Respond to requests for AirPlay. The AirPlayButton component triggers this event
// when the user clicks the AirPlay button.
player.on('airPlayRequested', onAirPlayRequested.bind(null, player));
}
/**
* Sets up the AirPlay plugin.
*
* @private
* @param player {object} the Video.js player
* @param options {object} the plugin options
*/
function enableAirPlay(player, options) {
if (!player.controlBar) {
return;
}
if (hasAirPlayAPISupport()) {
listenForAirPlayEvents(player);
ensureAirPlayButtonExists(player, options);
}
}
/**
* Registers the AirPlay plugin with Video.js. Calls
* {@link http://docs.videojs.com/module-videojs.html#~registerPlugin|videojs#registerPlugin},
* which will add a plugin function called `airPlay` to any instance of a Video.js player
* that is created after calling this function. Call `player.airPlay(options)`, passing in
* configuration options, to enable the AirPlay plugin on your Player instance.
*
* Currently, the only configuration option is:
*
* * **buttonText** - the text to display inside of the button component. By default,
* this text is hidden and is used for accessibility purposes.
*
* @param {object} videojs
* @see http://docs.videojs.com/module-videojs.html#~registerPlugin
*/
module.exports = function (videojs) {
videojs.registerPlugin('airPlay', function (options) {
var pluginOptions = Object.assign({
addButtonToControlBar: true
}, options || {});
// `this` is an instance of a Video.js Player.
// Wait until the player is "ready" so that the player's control bar component has
// been created.
this.ready(enableAirPlay.bind(this, this, pluginOptions));
});
};
},{"./lib/hasAirPlayAPISupport":4}],3:[function(require,module,exports){
"use strict";
var createAirPlayButton = require('./components/AirPlayButton'),
createAirPlayPlugin = require('./enableAirPlay');
/**
* @module index
*/
/**
* Registers the AirPlay plugin and AirPlayButton Component with Video.js. See
* {@link module:AirPlayButton} and {@link module:enableAirPlay} for more details about
* how the plugin and button are registered and configured.
*
* @param {object} videojs
* @see module:enableAirPlay
* @see module:AirPlayButton
*/
module.exports = function (videojs) {
videojs = videojs || window.videojs;
createAirPlayButton(videojs);
createAirPlayPlugin(videojs);
};
},{"./components/AirPlayButton":1,"./enableAirPlay":2}],4:[function(require,module,exports){
"use strict";
/**
* @module hasAirPlayAPISupport
*/
/**
* Returns whether or not the current browser environment supports AirPlay.
*
* @private
* @returns {boolean} true if AirPlay support is available
*/
module.exports = function () {
return !!window.WebKitPlaybackTargetAvailabilityEvent;
};
},{}],5:[function(require,module,exports){
"use strict";
/**
* This module is used as an entry point for the build system to bundle this plugin into a
* single javascript file that can be loaded by a script tag on a web page. The javascript
* file that is built assumes that `videojs` is available globally at `window.videojs`, so
* Video.js must be loaded **before** this plugin is loaded.
*
* Run `npm install` and then `grunt build` to build the plugin's bundled javascript
* file, as well as the CSS and image assets into the project's `./dist/` folder.
*
* @module standalone
*/
require('./index')();
},{"./index":3}]},{},[5]);

View File

@ -1 +0,0 @@
.vjs-airplay-button .vjs-icon-placeholder{background:url("ic_airplay_white_24px.svg") center center no-repeat;background-size:contain;display:inline-block;width:12px;height:12px}.vjs-airplay-button:hover{cursor:pointer}.vjs-airplay-button:hover .vjs-icon-placeholder{background-image:url("ic_airplay_white_24px.svg")}.vjs-airplay-button.vjs-airplay-button-lg:not(.vjs-hidden){display:flex;align-items:center;width:auto;padding:0 4px}.vjs-airplay-button.vjs-airplay-button-lg:not(.vjs-hidden) .vjs-airplay-button-label{flex-grow:1;margin-left:4px}.vjs-airplay-button.vjs-airplay-button-lg:not(.vjs-hidden) .vjs-icon-placeholder{flex-grow:1}

View File

@ -1,2 +0,0 @@
/*! @silvermine/videojs-airplay 2024-09-06 v1.3.0-8-gdbf8f7d */
!function n(a,r,o){function l(i,t){if(!r[i]){if(!a[i]){var e="function"==typeof require&&require;if(!t&&e)return e(i,!0);if(s)return s(i,!0);throw(t=new Error("Cannot find module '"+i+"'")).code="MODULE_NOT_FOUND",t}e=r[i]={exports:{}},a[i][0].call(e.exports,function(t){return l(a[i][1][t]||t)},e,e.exports,n,a,r,o)}return r[i].exports}for(var s="function"==typeof require&&require,t=0;t<o.length;t++)l(o[t]);return l}({1:[function(t,i,e){var n=t("../lib/hasAirPlayAPISupport");i.exports=function(t){var i=t.getComponent("Button");class e extends i{constructor(t,i){super(t,i),n()||this.hide(),this._reactToAirPlayAvailableEvents(),i.addAirPlayLabelToButton?(this.el().classList.add("vjs-airplay-button-lg"),this._labelEl=document.createElement("span"),this._labelEl.classList.add("vjs-airplay-button-label"),this._labelEl.textContent=this.localize("AirPlay"),this.el().appendChild(this._labelEl)):this.controlText("Start AirPlay")}buildCSSClass(){return"vjs-airplay-button "+super.buildCSSClass()}handleClick(){this.player().trigger("airPlayRequested")}_getMediaEl(){return this.player().el().querySelector("video, audio")}_reactToAirPlayAvailableEvents(){var t=this._getMediaEl(),i=this;function e(t){"available"===t.availability?i.show():i.hide()}t&&n()&&(t.addEventListener("webkitplaybacktargetavailabilitychanged",e),this.on("dispose",function(){t.removeEventListener("webkitplaybacktargetavailabilitychanged",e)}))}}t.registerComponent("airPlayButton",e)}},{"../lib/hasAirPlayAPISupport":4}],2:[function(t,i,e){var n=t("./lib/hasAirPlayAPISupport");function a(t){t.on("airPlayRequested",function(t){(t=t.el().querySelector("video, audio"))&&t.webkitShowPlaybackTargetPicker&&t.webkitShowPlaybackTargetPicker()}.bind(null,t))}i.exports=function(t){t.registerPlugin("airPlay",function(t){t=Object.assign({addButtonToControlBar:!0},t||{});this.ready(function(t,i){var e;t.controlBar&&n()&&(a(t),i=i,e=(t=t).controlBar.getChild("airPlayButton"),i.addButtonToControlBar)&&!e&&(e=t.controlBar.children().length,void 0!==i.buttonPositionIndex&&(e=0<=i.buttonPositionIndex?i.buttonPositionIndex:t.controlBar.children().length+i.buttonPositionIndex),t.controlBar.addChild("airPlayButton",i,e))}.bind(this,this,t))})}},{"./lib/hasAirPlayAPISupport":4}],3:[function(t,i,e){var n=t("./components/AirPlayButton"),a=t("./enableAirPlay");i.exports=function(t){t=t||window.videojs,n(t),a(t)}},{"./components/AirPlayButton":1,"./enableAirPlay":2}],4:[function(t,i,e){i.exports=function(){return!!window.WebKitPlaybackTargetAvailabilityEvent}},{}],5:[function(t,i,e){t("./index")()},{"./index":3}]},{},[5]);

View File

@ -1 +0,0 @@
.vjs-chromecast-button .vjs-icon-placeholder{background:url("ic_cast_white_24dp.png") center center no-repeat;background-size:contain;display:inline-block;width:12px;height:12px}.vjs-chromecast-button:hover{cursor:pointer}.vjs-chromecast-button:hover .vjs-icon-placeholder{background-image:url("ic_cast_white_24dp.png")}.vjs-chromecast-button.vjs-chromecast-casting-state .vjs-icon-placeholder{background-image:url("ic_cast_connected_white_24dp.png")}.vjs-chromecast-button.vjs-chromecast-casting-state:hover .vjs-icon-placeholder{background-image:url("ic_cast_connected_white_24dp.png")}.vjs-chromecast-button.vjs-chromecast-button-lg:not(.vjs-hidden){display:flex;align-items:center;width:auto;padding:0 4px}.vjs-chromecast-button.vjs-chromecast-button-lg:not(.vjs-hidden) .vjs-chromecast-button-label{flex-grow:1;margin-left:4px}.vjs-chromecast-button.vjs-chromecast-button-lg:not(.vjs-hidden) .vjs-icon-placeholder{flex-grow:1}.vjs-tech-chromecast{display:flex;flex-direction:column;justify-content:center;align-items:center;overflow:hidden}.vjs-tech-chromecast .vjs-tech-chromecast-poster::after{content:" ";display:block;height:2px;width:100px;background-color:#ccc;position:absolute;left:calc(50% - 50px)}.vjs-tech-chromecast .vjs-tech-chromecast-poster-img{max-height:180px;width:auto;border:2px solid #ccc}.vjs-tech-chromecast .vjs-tech-chromecast-poster-img.vjs-tech-chromecast-poster-img-empty{width:160px;height:90px}.vjs-tech-chromecast .vjs-tech-chromecast-title-container{position:absolute;bottom:50%;margin-bottom:100px;color:#ccc;text-align:center}.vjs-tech-chromecast .vjs-tech-chromecast-title{font-size:22px}.vjs-tech-chromecast .vjs-tech-chromecast-title.vjs-tech-chromecast-title-empty{display:none}.vjs-tech-chromecast .vjs-tech-chromecast-subtitle{font-size:18px;padding-top:.5em}.vjs-tech-chromecast .vjs-tech-chromecast-subtitle.vjs-tech-chromecast-subtitle-empty{display:none}

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
.vjs-chromecast-button .vjs-icon-placeholder{background:url("ic_cast_white_24dp.png") center center no-repeat;background-size:contain;display:inline-block;width:12px;height:12px}.vjs-chromecast-button:hover{cursor:pointer}.vjs-chromecast-button:hover .vjs-icon-placeholder{background-image:url("ic_cast_white_24dp.png")}.vjs-chromecast-button.vjs-chromecast-casting-state .vjs-icon-placeholder{background-image:url("ic_cast_connected_white_24dp.png")}.vjs-chromecast-button.vjs-chromecast-casting-state:hover .vjs-icon-placeholder{background-image:url("ic_cast_connected_white_24dp.png")}.vjs-chromecast-button.vjs-chromecast-button-lg:not(.vjs-hidden){display:flex;align-items:center;width:auto;padding:0 4px}.vjs-chromecast-button.vjs-chromecast-button-lg:not(.vjs-hidden) .vjs-chromecast-button-label{flex-grow:1;margin-left:4px}.vjs-chromecast-button.vjs-chromecast-button-lg:not(.vjs-hidden) .vjs-icon-placeholder{flex-grow:1}.vjs-tech-chromecast{display:flex;flex-direction:column;justify-content:center;align-items:center;overflow:hidden}.vjs-tech-chromecast .vjs-tech-chromecast-poster::after{content:" ";display:block;height:2px;width:100px;background-color:#ccc;position:absolute;left:calc(50% - 50px)}.vjs-tech-chromecast .vjs-tech-chromecast-poster-img{max-height:180px;width:auto;border:2px solid #ccc}.vjs-tech-chromecast .vjs-tech-chromecast-poster-img.vjs-tech-chromecast-poster-img-empty{width:160px;height:90px}.vjs-tech-chromecast .vjs-tech-chromecast-title-container{position:absolute;bottom:50%;margin-bottom:100px;color:#ccc;text-align:center}.vjs-tech-chromecast .vjs-tech-chromecast-title{font-size:22px}.vjs-tech-chromecast .vjs-tech-chromecast-title.vjs-tech-chromecast-title-empty{display:none}.vjs-tech-chromecast .vjs-tech-chromecast-subtitle{font-size:18px;padding-top:.5em}.vjs-tech-chromecast .vjs-tech-chromecast-subtitle.vjs-tech-chromecast-subtitle-empty{display:none}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,10 +5,3 @@ dist/videojs-overlay.min.css
dist/video-js-skin.min.css dist/video-js-skin.min.css
dist/videojs-license.min.js dist/videojs-license.min.js
dist/videojs-license.min.css dist/videojs-license.min.css
dist/videojs-chromecast.min.js
dist/videojs-chromecast.min.css
dist/ic_cast_connected_white_24dp.png
dist/ic_cast_white_24dp.png
dist/videojs-airplay.min.js
dist/videojs-airplay.min.css
dist/ic_airplay_white_24px.svg

View File

@ -15,12 +15,11 @@
<link href="player/videojs/dist/video-js-skin.min.css" rel="stylesheet"> <link href="player/videojs/dist/video-js-skin.min.css" rel="stylesheet">
<link href="player/videojs/dist/videojs-overlay.min.css" rel="stylesheet"> <link href="player/videojs/dist/videojs-overlay.min.css" rel="stylesheet">
<link href="player/videojs/dist/videojs-license.min.css" rel="stylesheet"> <link href="player/videojs/dist/videojs-license.min.css" rel="stylesheet">
{{#if airplay}} <style>
<link href="player/videojs/dist/videojs-airplay.min.css" rel="stylesheet"> .player-poster[data-poster] .poster-background[data-poster] {
{{/if}} height: initial !important;
{{#if chromecast}} }
<link href="player/videojs/dist/videojs-chromecast.min.css" rel="stylesheet"> </style>
{{/if}}
</head> </head>
<body> <body>
<div style="position:absolute;top:0;right:0;bottom:0;left:0"> <div style="position:absolute;top:0;right:0;bottom:0;left:0">
@ -29,13 +28,6 @@
<script src="player/videojs/dist/video.min.js"></script> <script src="player/videojs/dist/video.min.js"></script>
<script src="player/videojs/dist/videojs-overlay.min.js"></script> <script src="player/videojs/dist/videojs-overlay.min.js"></script>
<script src="player/videojs/dist/videojs-license.min.js"></script> <script src="player/videojs/dist/videojs-license.min.js"></script>
{{#if airplay}}
<script src='player/videojs/dist/videojs-airplay.min.js'></script>
{{/if}}
{{#if chromecast}}
<script src='player/videojs/dist/videojs-chromecast.min.js'></script>
<script src='https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'></script>
{{/if}}
<script> <script>
function getQueryParam(key, defaultValue) { function getQueryParam(key, defaultValue) {
var query = window.location.search.substring(1); var query = window.location.search.substring(1);
@ -82,20 +74,12 @@
liveui: true, liveui: true,
responsive: true, responsive: true,
fluid: true, fluid: true,
sources: [{ src: window.location.origin + '/' + playerConfig.source, type: 'application/x-mpegURL' }], sources: [{ src: playerConfig.source, type: 'application/x-mpegURL' }],
plugins: {
license: playerConfig.license
}
}; };
if (playerConfig.chromecast) {
config.techOrder = ["chromecast", "html5"];
config.plugins.chromecast = {
receiverApplicationId: 'CC1AD845'
};
}
if (playerConfig.airplay) {
config.plugins.airPlay = {};
}
var player = videojs('player', config); var player = videojs('player', config);
player.ready(function() { player.ready(function() {
if(playerConfig.logo.image.length != 0) { if(playerConfig.logo.image.length != 0) {
@ -129,8 +113,6 @@
}, },
], ],
}); });
player.license(playerConfig.license);
} }
if (autoplay === true) { if (autoplay === true) {

View File

@ -0,0 +1,67 @@
var plugins = [];
if (statistics == true) {
plugins.push(ClapprNerdStats);
plugins.push(ClapprStats);
}
var config = {
source: playerConfig.source,
parentId: '#player',
baseUrl: 'clappr/',
persistConfig: false,
plugins: plugins,
poster: playerConfig.poster + '?t=' + String(new Date().getTime()),
mediacontrol: {
seekbar: playerConfig.color.seekbar,
buttons: color,
},
height: '100%',
width: '100%',
disableCanAutoPlay: true,
autoPlay: autoplay,
mimeType: 'application/vnd.apple.mpegurl',
actualLiveTime: false,
exitFullscreenOnEnd: false,
mute: mute,
playback: {
controls: false,
playInline: true,
recycleVideo: Clappr.Browser.isMobile,
hlsjsConfig: {
enableWorker: false,
capLevelToPlayerSize: true,
capLevelOnFPSDrop: true,
maxBufferHole: 1,
highBufferWatchdogPeriod: 1,
},
},
hlsPlayback: {
preload: false,
},
visibilityEnableIcon: false,
clapprStats: {
runEach: 1000,
onReport: (metrics) => {},
},
clapprNerdStats: {
shortcut: ['command+shift+s', 'ctrl+shift+s'],
iconPosition: 'top-right',
},
};
if (playerConfig.logo.image.length != 0) {
config.watermark = playerConfig.logo.image;
config.position = playerConfig.logo.position;
if (playerConfig.logo.link.length != 0) {
config.watermarkLink = playerConfig.logo.link;
}
}
var player = new window.Clappr.Player(config);
var posterPlugin = player.core.mediaControl.container.getPlugin('poster');
player.on(window.Clappr.Events.PLAYER_STOP, function updatePoster() {
posterPlugin.options.poster = playerConfig.poster + '?t=' + String(new Date().getTime());
posterPlugin.render();
});

View File

@ -44,12 +44,6 @@
<link href="player/videojs/dist/video-js-skin.min.css" rel="stylesheet"> <link href="player/videojs/dist/video-js-skin.min.css" rel="stylesheet">
<link href="player/videojs/dist/videojs-overlay.min.css" rel="stylesheet"> <link href="player/videojs/dist/videojs-overlay.min.css" rel="stylesheet">
<link href="player/videojs/dist/videojs-license.min.css" rel="stylesheet"> <link href="player/videojs/dist/videojs-license.min.css" rel="stylesheet">
{{#if airplay}}
<link href="player/videojs/dist/videojs-airplay.min.css" rel="stylesheet">
{{/if}}
{{#if chromecast}}
<link href="player/videojs/dist/videojs-chromecast.min.css" rel="stylesheet">
{{/if}}
{{/ifEquals}} {{/ifEquals}}
<style> <style>
/** flexboxgrid 6.3.1 **/ /** flexboxgrid 6.3.1 **/
@ -379,7 +373,7 @@
<div class="col-xs-12 player-l2"> <div class="col-xs-12 player-l2">
<div class="player-l3"> <div class="player-l3">
{{#ifEquals player "videojs"}} {{#ifEquals player "videojs"}}
<video id="player" class="vjs-public video-js vjs-16-9 player-l4" playsinline></video> <video id="player" class="vjs-public video-js player-l4" playsinline></video>
{{else}} {{else}}
<div id="player" class="player-l4"></div> <div id="player" class="player-l4"></div>
{{/ifEquals}} {{/ifEquals}}
@ -438,11 +432,11 @@
</div> </div>
<div class="col-xs-12 channel-list-inside"> <div class="col-xs-12 channel-list-inside">
{{#each channels}} {{#each channels}}
<a class="channel-list-link" href="playersite_{{this.channelid}}.html" target="_self"> <a class="channel-list-link" href="/playersite_{{this.channelid}}.html" target="_self">
<div class="row channel-list-l1 {{#ifEquals @root.channel_id this.channelid}}channel-list-selected{{/ifEquals}}"> <div class="row channel-list-l1 {{#ifEquals @root.channel_id this.channelid}}channel-list-selected{{/ifEquals}}">
<div class="col-xs-12 middle-xs channel-list-l2"> <div class="col-xs-12 middle-xs channel-list-l2">
<span id="channel_list_badge_{{this.channelid}}_1" class="channel-list-badge">LIVE</span> <span id="channel_list_badge_{{this.channelid}}_1" class="channel-list-badge">LIVE</span>
<img src="memfs/{{this.channelid}}.jpg" class="channel-list-image"> <img src="/memfs/{{this.channelid}}.jpg" class="channel-list-image">
<div class="row"> <div class="row">
<div class="channel-list-description"> <div class="channel-list-description">
{{this.name}} {{this.name}}
@ -498,11 +492,11 @@
</div> </div>
<div class="col-xs-12 col-sm-12"> <div class="col-xs-12 col-sm-12">
{{#each channels}} {{#each channels}}
<a class="channel-list-link" href="playersite_{{this.channelid}}.html" target="_self"> <a class="channel-list-link" href="/playersite_{{this.channelid}}.html" target="_self">
<div class="row channel-list-l1 {{#ifEquals @root.channel_id this.channelid}}channel-list-selected{{/ifEquals}}"> <div class="row channel-list-l1 {{#ifEquals @root.channel_id this.channelid}}channel-list-selected{{/ifEquals}}">
<div class="col-xs-12 middle-xs channel-list-l2"> <div class="col-xs-12 middle-xs channel-list-l2">
<span id="channel_list_badge_{{this.channelid}}_2" class="channel-list-badge">LIVE</span> <span id="channel_list_badge_{{this.channelid}}_2" class="channel-list-badge">LIVE</span>
<img src="memfs/{{this.channelid}}.jpg" class="channel-list-image"> <img src="/memfs/{{this.channelid}}.jpg" class="channel-list-image">
<div class="row"> <div class="row">
<div class="channel-list-description"> <div class="channel-list-description">
{{this.name}} {{this.name}}
@ -619,7 +613,6 @@
{{/if}}{{/if}} {{/if}}{{/if}}
<script> <script>
{{#if share}}
var modal_share = document.getElementById("modal-share"); var modal_share = document.getElementById("modal-share");
var btn_share = document.getElementById("btn-share"); var btn_share = document.getElementById("btn-share");
var close_share = document.getElementById("close-share"); var close_share = document.getElementById("close-share");
@ -629,7 +622,6 @@
close_share.onclick = function() { close_share.onclick = function() {
modal_share.style.display = "none"; modal_share.style.display = "none";
} }
{{/if}}
{{#if imprint_html}} {{#if imprint_html}}
var modal_imprint = document.getElementById("modal-imprint"); var modal_imprint = document.getElementById("modal-imprint");
var btn_imprint = document.getElementById("btn-imprint"); var btn_imprint = document.getElementById("btn-imprint");
@ -666,9 +658,7 @@
var close = document.getElementsByClassName("close")[0]; var close = document.getElementsByClassName("close")[0];
close.onclick = function() { close.onclick = function() {
{{#if share}}
modal_share.style.display = "none"; modal_share.style.display = "none";
{{/if}}
{{#if imprint_html}} {{#if imprint_html}}
modal_imprint.style.display = "none"; modal_imprint.style.display = "none";
{{/if}} {{/if}}
@ -680,29 +670,20 @@
{{/if}}{{/if}} {{/if}}{{/if}}
} }
window.onclick = function(event) { window.onclick = function(event) {
switch(event.target) { if (event.target == modal_share) {
{{#if share}} modal_share.style.display = "none";
case modal_share: {{#if imprint_html}}
modal_share.style.display = "none"; } else if (event.target == modal_imprint) {
break; modal_imprint.style.display = "none";
{{/if}} {{/if}}
{{#if imprint_html}} {{#if terms_html}}
case modal_imprint: } else if (event.target == modal_terms) {
modal_imprint.style.display = "none"; modal_terms.style.display = "none";
break; {{/if}}
{{/if}} {{#if channel_creator_name}}{{#if channel_creator_description_html}}
{{#if terms_html}} } else if (event.target == modal_creator) {
case modal_terms: modal_creator.style.display = "none";
modal_terms.style.display = "none"; {{/if}}{{/if}}
break;
{{/if}}
{{#if channel_creator_name}}{{#if channel_creator_description_html}}
case modal_creator:
modal_creator.style.display = "none";
break;
{{/if}}{{/if}}
default:
break;
} }
} }
</script> </script>
@ -711,10 +692,10 @@
var license = '{{channel_license}}'; var license = '{{channel_license}}';
var license_url = ''; var license_url = '';
var license_name = 'unknown'; var license_name = 'unknown';
if (license === 'CC0 1.0') { if (license === 'CC0 4.0') {
license_name = 'CC0 1.0 Universal' license_name = 'CC0 1.0 Universal'
license_url = 'https://creativecommons.org/publicdomain/zero/1.0/'; license_url = 'https://creativecommons.org/publicdomain/zero/1.0/';
license_image = 'https://mirrors.creativecommons.org/presskit/buttons/88x31/png/pd.png'; license_image = 'https://creativecommons.org/publicdomain/zero/1.0/';
} else if (license === 'CC BY 4.0') { } else if (license === 'CC BY 4.0') {
license_name = 'CC BY 4.0' license_name = 'CC BY 4.0'
license_url = 'https://creativecommons.org/licenses/by/4.0/'; license_url = 'https://creativecommons.org/licenses/by/4.0/';
@ -738,7 +719,7 @@
} else if (license === 'CC BY-NC-ND 4.0') { } else if (license === 'CC BY-NC-ND 4.0') {
license_name = 'CC BY-NC-ND 4.0'; license_name = 'CC BY-NC-ND 4.0';
license_url = 'https://creativecommons.org/licenses/by-nc-nd/4.0/'; license_url = 'https://creativecommons.org/licenses/by-nc-nd/4.0/';
license_image = 'https://mirrors.creativecommons.org/presskit/buttons/88x31/png/by-nc-nd.png'; license_image = 'https://creativecommons.org/licenses/by-nc-nd/4.0/';
} }
document.getElementById("license").setAttribute("href", license_url); document.getElementById("license").setAttribute("href", license_url);
document.getElementById("license_image").src = license_image; document.getElementById("license_image").src = license_image;
@ -801,13 +782,6 @@
<script src="player/videojs/dist/video.min.js"></script> <script src="player/videojs/dist/video.min.js"></script>
<script src="player/videojs/dist/videojs-overlay.min.js"></script> <script src="player/videojs/dist/videojs-overlay.min.js"></script>
<script src="player/videojs/dist/videojs-license.min.js"></script> <script src="player/videojs/dist/videojs-license.min.js"></script>
{{#if airplay}}
<script src='player/videojs/dist/videojs-airplay.min.js'></script>
{{/if}}
{{#if chromecast}}
<script src='player/videojs/dist/videojs-chromecast.min.js'></script>
<script src='https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1'></script>
{{/if}}
{{else}} {{else}}
<script src="player/clappr/clappr.min.js"></script> <script src="player/clappr/clappr.min.js"></script>
<script src="player/clappr/clappr-stats.min.js"></script> <script src="player/clappr/clappr-stats.min.js"></script>
@ -850,8 +824,6 @@
var mute = convertBoolParam("mute", playerConfig.mute); var mute = convertBoolParam("mute", playerConfig.mute);
var statistics = convertBoolParam("stats", playerConfig.statistics); var statistics = convertBoolParam("stats", playerConfig.statistics);
var color = convertColorParam("color", playerConfig.color.buttons); var color = convertColorParam("color", playerConfig.color.buttons);
var chromecast = {{#if chromecast}}true{{else}}false{{/if}};
var airplay = {{#if airplay}}true{{else}}false{{/if}};
</script> </script>
<script src="playersite/player.js"></script> <script src="playersite/player.js"></script>

View File

@ -2,36 +2,18 @@ var config = {
controls: true, controls: true,
poster: playerConfig.poster + '?t=' + String(new Date().getTime()), poster: playerConfig.poster + '?t=' + String(new Date().getTime()),
autoplay: autoplay ? 'muted' : false, autoplay: autoplay ? 'muted' : false,
muted: true, muted: mute,
liveui: true, liveui: true,
responsive: true, responsive: true,
fluid: true, fluid: true,
// Needed to append the url origin in order for the source to properly pass to the cast device sources: [{ src: playerConfig.source, type: 'application/x-mpegURL' }],
sources: [{ src: window.location.origin + '/' + playerConfig.source, type: 'application/x-mpegURL' }], plugins: {
plugins: {}, license: playerConfig.license,
},
}; };
if (chromecast) {
config.techOrder = ['chromecast', 'html5'];
// Provide a default reciever application ID
config.plugins.chromecast = {
receiverApplicationId: 'CC1AD845',
};
}
var player = videojs('player', config); var player = videojs('player', config);
player.ready(function () { player.ready(function () {
if (chromecast) {
player.chromecast();
}
if (airplay) {
player.airPlay();
}
player.license(playerConfig.license);
if (playerConfig.logo.image.length != 0) { if (playerConfig.logo.image.length != 0) {
var overlay = null; var overlay = null;

View File

@ -4,7 +4,7 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="favicon.ico" /> <link rel="icon" href="favicon.ico" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" /> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
<meta name="theme-color" content="#282728" /> <meta name="theme-color" content="#000000" />
<meta name="description" content="Restreamer Video-Streaming" /> <meta name="description" content="Restreamer Video-Streaming" />
<link rel="apple-touch-icon" href="logo192.png" /> <link rel="apple-touch-icon" href="logo192.png" />
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="manifest.json" />

View File

@ -20,6 +20,6 @@
], ],
"start_url": ".", "start_url": ".",
"display": "standalone", "display": "standalone",
"theme_color": "#282728", "theme_color": "#000000",
"background_color": "#282728" "background_color": "#ffffff"
} }

View File

@ -2,63 +2,45 @@ import React from 'react';
import { I18nProvider } from '@lingui/react'; import { I18nProvider } from '@lingui/react';
import { i18n } from '@lingui/core'; import { i18n } from '@lingui/core';
import * as plurals from 'make-plural/plurals';
import { messages as EN } from './locales/en/messages.js'; import { messages as EN } from './locales/en/messages.js';
import { messages as DA } from './locales/da/messages.js';
import { messages as DE } from './locales/de/messages.js'; import { messages as DE } from './locales/de/messages.js';
import { messages as EL } from './locales/el/messages.js';
import { messages as ES } from './locales/es/messages.js'; import { messages as ES } from './locales/es/messages.js';
import { messages as FR } from './locales/fr/messages.js'; import { messages as FR } from './locales/fr/messages.js';
import { messages as IT } from './locales/it/messages.js'; import { messages as IT } from './locales/it/messages.js';
import { messages as KO } from './locales/ko/messages.js';
import { messages as PL } from './locales/pl/messages.js'; import { messages as PL } from './locales/pl/messages.js';
import { messages as PT } from './locales/pt-br/messages.js'; import { messages as PT } from './locales/pt/messages.js';
import { messages as RU } from './locales/ru/messages.js'; import { messages as RU } from './locales/ru/messages.js';
import { messages as SL } from './locales/sl/messages.js';
import { messages as TR } from './locales/tr/messages.js';
import { messages as UK } from './locales/uk/messages.js';
import { messages as ZH } from './locales/zh-hans/messages.js';
import * as Storage from './utils/storage'; import * as Storage from './utils/storage';
i18n.loadLocaleData('en', { plurals: plurals.en });
i18n.loadLocaleData('de', { plurals: plurals.de });
i18n.loadLocaleData('es', { plurals: plurals.es });
i18n.loadLocaleData('fr', { plurals: plurals.fr });
i18n.loadLocaleData('it', { plurals: plurals.it });
i18n.loadLocaleData('pl', { plurals: plurals.pl });
i18n.loadLocaleData('pt', { plurals: plurals.pt });
i18n.loadLocaleData('ru', { plurals: plurals.ru });
i18n.load({ i18n.load({
en: EN, en: EN,
da: DA,
de: DE, de: DE,
el: EL,
es: ES, es: ES,
fr: FR, fr: FR,
it: IT, it: IT,
ko: KO,
pl: PL, pl: PL,
'pt-br': PT, pt: PT,
ru: RU, ru: RU,
sl: SL,
tr: TR,
uk: UK,
'zh-hans': ZH,
}); });
const aliases = { const getLanguage = (defaultLanguage, supportedLanguages) => {
pt: 'pt-br', let lang = Storage.Get('language');
'zh-cn': 'zh-hans', if (supportedLanguages.indexOf(lang) === -1) {
}; lang = getBrowserLanguage(defaultLanguage);
const getAlias = (lang) => {
if (lang in aliases) {
return aliases[lang];
} }
return lang;
};
const getLanguage = (defaultLanguage, supportedLanguages) => {
let lang = getAlias(Storage.Get('language'));
if (supportedLanguages.indexOf(lang) === -1) { if (supportedLanguages.indexOf(lang) === -1) {
lang = getAlias(getBrowserLanguage(defaultLanguage)); lang = defaultLanguage;
if (supportedLanguages.indexOf(lang) === -1) {
lang = defaultLanguage;
}
} }
Storage.Set('language', lang); Storage.Set('language', lang);
@ -69,7 +51,7 @@ const getLanguage = (defaultLanguage, supportedLanguages) => {
const getBrowserLanguage = (defaultLanguage) => { const getBrowserLanguage = (defaultLanguage) => {
let lang = window.navigator.language; let lang = window.navigator.language;
const match = lang.match(/^[a-z]+(-[a-z]+)?/i); const match = lang.match(/^[a-z]+/);
if (!match) { if (!match) {
return defaultLanguage; return defaultLanguage;
} }
@ -77,7 +59,7 @@ const getBrowserLanguage = (defaultLanguage) => {
return match[0].toLowerCase(); return match[0].toLowerCase();
}; };
i18n.activate(getLanguage('en', ['en', 'da', 'de', 'el', 'es', 'fr', 'it', 'ko', 'pl', 'pt-br', 'ru', 'sl', 'tr', 'uk', 'zh-hans'])); i18n.activate(getLanguage('en', ['en', 'de', 'es', 'fr', 'it', 'pl', 'pt', 'ru']));
export default function Provider(props) { export default function Provider(props) {
return <I18nProvider i18n={i18n}>{props.children}</I18nProvider>; return <I18nProvider i18n={i18n}>{props.children}</I18nProvider>;

View File

@ -14,15 +14,9 @@ import ChannelList from './misc/ChannelList';
import Footer from './Footer'; import Footer from './Footer';
import I18n from './I18n'; import I18n from './I18n';
import Header from './Header'; import Header from './Header';
import * as M from './utils/metadata';
import Restreamer from './utils/restreamer'; import Restreamer from './utils/restreamer';
import Router from './Router'; import Router from './Router';
import Views from './views'; import Views from './views';
import { UI as Version } from './version';
import Changelog from './misc/Changelog';
import SemverGt from 'semver/functions/gt';
import SemverValid from 'semver/functions/valid';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
MainHeader: { MainHeader: {
@ -64,12 +58,6 @@ export default function RestreamerUI(props) {
channelid: '', channelid: '',
channels: [], channels: [],
}); });
const [$metadata, setMetadata] = React.useState({});
const [$changelog, setChangelog] = React.useState({
open: false,
current: '',
previous: '',
});
const restreamer = React.useRef(null); const restreamer = React.useRef(null);
@ -143,8 +131,6 @@ export default function RestreamerUI(props) {
const valid = await restreamer.current.Validate(); const valid = await restreamer.current.Validate();
await checkChangelog();
setState({ setState({
...$state, ...$state,
initialized: true, initialized: true,
@ -160,93 +146,8 @@ export default function RestreamerUI(props) {
setReady(true); setReady(true);
}; };
const checkChangelog = async () => {
let showChangelog = true;
if (restreamer.current.IsConnected() === true) {
let metadata = await restreamer.current.GetMetadata(false);
const channels = await restreamer.current.ListChannels();
let current = Version.replace('restreamer-', '');
let previous = '';
if (SemverValid(current) === null) {
showChangelog = false;
}
if (metadata === null) {
if (channels.length === 1) {
const progress = await restreamer.current.GetIngestProgress(channels[0].channelid);
if (progress.valid === false) {
// assume fresh installation
metadata = M.initMetadata(metadata);
await restreamer.current.SetMetadata({
...metadata,
bundle: {
...metadata.bundle,
version: current,
},
});
return false;
}
}
}
metadata = M.initMetadata(metadata);
if ('version' in metadata.bundle) {
if (SemverValid(metadata.bundle.version) !== null) {
previous = metadata.bundle.version;
}
}
if (showChangelog === true) {
if (SemverValid(previous) === null) {
previous = '';
}
if (previous.length !== 0) {
if (!SemverGt(current, previous)) {
showChangelog = false;
}
}
}
setMetadata({
...$metadata,
...metadata,
});
setChangelog({
...$changelog,
open: showChangelog,
current: current,
previous: previous,
});
}
return showChangelog;
};
const handleCloseChangelog = async () => {
await restreamer.current.SetMetadata({
...$metadata,
bundle: {
...$metadata.bundle,
version: $changelog.current,
},
});
setChangelog({
...$changelog,
open: false,
});
};
const handleLogin = async (username, password) => { const handleLogin = async (username, password) => {
const connected = await restreamer.current.Login(username, password); const connected = await restreamer.current.Login(username, password);
await checkChangelog();
setState({ setState({
...$state, ...$state,
connected: connected, connected: connected,
@ -295,25 +196,16 @@ export default function RestreamerUI(props) {
}); });
}; };
const handlePasswordReset = async (username, loginUsername, password, loginPassword) => { const handlePasswordReset = async (username, password) => {
const data = { const [, err] = await restreamer.current.ConfigSet({
api: { api: {
auth: { auth: {
enable: true, enable: true,
username: username,
password: password,
}, },
}, },
version: 3, });
};
if (username.length !== 0) {
data.api.auth.username = username;
}
if (password.length !== 0) {
data.api.auth.password = password;
}
const [, err] = await restreamer.current.ConfigSet(data);
if (err !== null) { if (err !== null) {
notify('error', 'save:settings', `There was an error resetting the password.`); notify('error', 'save:settings', `There was an error resetting the password.`);
return 'ERROR'; return 'ERROR';
@ -357,7 +249,7 @@ export default function RestreamerUI(props) {
if (restarted === true) { if (restarted === true) {
// After the restart the API requires a login and this means the restart happened // After the restart the API requires a login and this means the restart happened
await restreamer.current.Validate(); await restreamer.current.Validate();
await restreamer.current.Login(loginUsername, loginPassword); await restreamer.current.Login(username, password);
window.location.reload(); window.location.reload();
} else { } else {
@ -456,42 +348,30 @@ export default function RestreamerUI(props) {
return null; return null;
}; };
let view = null; let view = <Views.Initializing />;
if ($state.initialized === false) { if ($state.valid === false) {
view = <Views.Initializing />; view = <Views.Invalid address={restreamer.current.Address()} />;
} else { } else if ($state.connected === false) {
if ($state.valid === false) { view = (
view = <Views.Invalid address={restreamer.current.Address()} />; <Views.Login
} else if ($state.connected === false) { onLogin={handleLogin}
view = ( auths={restreamer.current.Auths()}
<Views.Login hasService={$state.service}
onLogin={handleLogin} address={restreamer.current.Address()}
auths={restreamer.current.Auths()} onAuth0={handleAuth0}
hasService={$state.service} />
address={restreamer.current.Address()} );
onAuth0={handleAuth0} } else if ($state.compatibility.compatible === false) {
/> if ($state.compatibility.core.compatible === false) {
); view = <Views.Incompatible type="core" have={$state.compatibility.core.have} want={$state.compatibility.core.want} />;
} else if ($state.compatibility.compatible === false) { } else if ($state.compatibility.ffmpeg.compatible === false) {
if ($state.compatibility.core.compatible === false) { view = <Views.Incompatible type="ffmpeg" have={$state.compatibility.ffmpeg.have} want={$state.compatibility.ffmpeg.want} />;
view = <Views.Incompatible type="core" have={$state.compatibility.core.have} want={$state.compatibility.core.want} />;
} else if ($state.compatibility.ffmpeg.compatible === false) {
view = <Views.Incompatible type="ffmpeg" have={$state.compatibility.ffmpeg.have} want={$state.compatibility.ffmpeg.want} />;
}
} else if ($state.password === true) {
view = (
<Views.Password
onReset={handlePasswordReset}
username={restreamer.current.ConfigValue('api.auth.username')}
usernameOverride={restreamer.current.ConfigOverrides('api.auth.username')}
password={restreamer.current.ConfigValue('api.auth.password')}
passwordOverride={restreamer.current.ConfigOverrides('api.auth.password')}
/>
);
} else {
view = <Router restreamer={restreamer.current} />;
resources = handleResources;
} }
} else if ($state.password === true) {
view = <Views.Password onReset={handlePasswordReset} />;
} else {
view = <Router restreamer={restreamer.current} />;
resources = handleResources;
} }
const expand = $state.connected && $state.compatibility.compatible && !$state.password; const expand = $state.connected && $state.compatibility.compatible && !$state.password;
@ -548,9 +428,6 @@ export default function RestreamerUI(props) {
onState={handleStateChannel} onState={handleStateChannel}
/> />
)} )}
{expand && $changelog.open && (
<Changelog open={$changelog.open} onClose={handleCloseChangelog} current={$changelog.current} previous={$changelog.previous} />
)}
</NotifyProvider> </NotifyProvider>
</I18n> </I18n>
); );

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Route, Navigate, Routes, HashRouter as DOMRouter } from 'react-router-dom'; import { Route, Navigate, Routes, HashRouter } from 'react-router-dom';
import Views from './views'; import Views from './views';
@ -11,9 +11,9 @@ export default function Router(props) {
const channelid = props.restreamer.GetCurrentChannelID(); const channelid = props.restreamer.GetCurrentChannelID();
return ( return (
<DOMRouter> <HashRouter>
<Routes> <Routes>
<Route path="/" element={<Views.ChannelSelect channelid={channelid} />} /> <Route path="/" element={<Views.ChannelSelect restreamer={props.restreamer} />} />
<Route path="/playersite" element={<Views.Playersite restreamer={props.restreamer} />} /> <Route path="/playersite" element={<Views.Playersite restreamer={props.restreamer} />} />
<Route path="/settings" element={<Views.Settings restreamer={props.restreamer} />} /> <Route path="/settings" element={<Views.Settings restreamer={props.restreamer} />} />
<Route path="/settings/:tab" element={<Views.Settings restreamer={props.restreamer} />} /> <Route path="/settings/:tab" element={<Views.Settings restreamer={props.restreamer} />} />
@ -26,7 +26,7 @@ export default function Router(props) {
<Route path="/:channelid/publication/:service/:index" element={<Views.EditService key={channelid} restreamer={props.restreamer} />} /> <Route path="/:channelid/publication/:service/:index" element={<Views.EditService key={channelid} restreamer={props.restreamer} />} />
<Route path="*" render={() => <Navigate to="/" replace />} /> <Route path="*" render={() => <Navigate to="/" replace />} />
</Routes> </Routes>
</DOMRouter> </HashRouter>
); );
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { createRoot } from 'react-dom/client'; import ReactDOM from 'react-dom';
import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles'; import { ThemeProvider, StyledEngineProvider } from '@mui/material/styles';
import '@fontsource/dosis'; import '@fontsource/dosis';
@ -10,20 +10,18 @@ import theme from './theme';
import RestreamerUI from './RestreamerUI'; import RestreamerUI from './RestreamerUI';
let address = window.location.protocol + '//' + window.location.host; let address = window.location.protocol + '//' + window.location.host;
if (window.location.pathname.endsWith('/ui/')) {
address += window.location.pathname.replace(/ui\/$/, '');
}
const urlParams = new URLSearchParams(window.location.search.substring(1)); const urlParams = new URLSearchParams(window.location.search.substring(1));
if (urlParams.has('address') === true) { if (urlParams.has('address') === true) {
address = urlParams.get('address'); address = urlParams.get('address');
} }
createRoot(document.getElementById('root')).render( ReactDOM.render(
<StyledEngineProvider injectFirst> <StyledEngineProvider injectFirst>
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<CssBaseline /> <CssBaseline />
<RestreamerUI address={address} /> <RestreamerUI address={address} />
</ThemeProvider> </ThemeProvider>
</StyledEngineProvider> </StyledEngineProvider>,
document.getElementById('root')
); );

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -47,9 +47,8 @@ export default function Component(props) {
return ( return (
<Stack <Stack
direction="column" direction="column"
justifyContent={props.justifyContent} justifyContent="center"
alignItems={props.alignItems} alignItems="center"
textAlign={props.textAlign}
spacing={1} spacing={1}
className={ className={
props.color === 'dark' ? classes.dark : props.color === 'success' ? classes.success : props.color === 'danger' ? classes.danger : classes.light props.color === 'dark' ? classes.dark : props.color === 'success' ? classes.success : props.color === 'danger' ? classes.danger : classes.light
@ -63,7 +62,4 @@ export default function Component(props) {
Component.defaultProps = { Component.defaultProps = {
color: 'light', color: 'light',
textAlign: 'left',
alignItems: 'center',
justifyContent: 'center',
}; };

View File

@ -1,197 +0,0 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import makeStyles from '@mui/styles/makeStyles';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import ReactMarkdown from 'react-markdown';
import SemverGt from 'semver/functions/gt';
import SemverLte from 'semver/functions/lte';
import SemverEq from 'semver/functions/eq';
import SemverValid from 'semver/functions/valid';
import BoxText from './BoxText';
import Dialog from './modals/Dialog';
const useStyles = makeStyles((theme) => ({
h1: {
fontFamily: theme.typography.h1.fontFamily,
fontSize: theme.typography.h1.fontSize,
marginTop: '.5rem',
marginBottom: '-1rem',
},
h2: {
fontFamily: theme.typography.h2.fontFamily,
fontSize: theme.typography.h2.fontSize,
paddingTop: '1.5rem',
marginBottom: theme.typography.h2.marginBottom,
'&::after': {
content: '" "',
display: 'block',
height: 1,
backgroundColor: theme.palette.primary.contrastText,
},
},
h3: {
fontFamily: theme.typography.h3.fontFamily,
fontSize: theme.typography.h3.fontSize,
paddingTop: '.5rem',
marginBottom: theme.typography.h3.marginBottom,
},
h4: {
fontFamily: theme.typography.h4.fontFamily,
fontSize: theme.typography.h4.fontSize,
marginBottom: theme.typography.h4.marginBottom,
},
a: {
fontWeight: 'bold',
color: theme.palette.secondary.main,
},
}));
export default function Changelog(props) {
const [$data, setData] = React.useState('');
const classes = useStyles();
React.useEffect(() => {
(async () => {
await onMount();
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const onMount = async () => {
let data = await loadData();
data = filter(data, props.current, props.previous);
setData(data);
};
const loadData = async () => {
let response = null;
try {
response = await fetch('CHANGELOG.md', {
method: 'GET',
});
} catch (err) {
return '';
}
if (response.ok === false) {
return '';
}
return await response.text();
};
const filter = (data, current, previous) => {
let lines = data.split('\n');
let filteredLines = [];
let copy = true;
for (let i = 0; i < lines.length; i++) {
if (lines[i].startsWith('## ')) {
let version = lines[i].replace('## ', '');
if (SemverValid(version) === null) {
if (copy === true) {
filteredLines.push(lines[i]);
}
continue;
}
if (current.length === 0) {
current = version;
}
if (previous.length === 0) {
previous = version;
}
if (SemverEq(current, previous)) {
if (SemverEq(version, current)) {
copy = true;
} else {
copy = false;
}
} else {
if (SemverLte(version, current) && SemverGt(version, previous)) {
copy = true;
} else {
copy = false;
}
}
}
if (copy === true) {
filteredLines.push(lines[i]);
}
}
return filteredLines.join('\n');
};
if ($data.length === 0 || $data.startsWith('<!DOCTYPE')) {
return null;
}
const renderers = {
h1: (props) => (
<h1 className={classes.h1} {...props}>
{props.children}
</h1>
),
h2: (props) => (
<h2 className={classes.h2} {...props}>
{props.children}
</h2>
),
h3: (props) => (
<h3 className={classes.h3} {...props}>
{props.children}
</h3>
),
h4: (props) => (
<h4 className={classes.h4} {...props}>
{props.children}
</h4>
),
a: (props) => (
<a className={classes.a} target="_blank" {...props}>
{props.children}
</a>
),
};
return (
<Dialog
open={props.open}
onClose={props.onClose}
title={<Trans>Update details (Changelog)</Trans>}
maxWidth={600}
buttonsRight={
<Button variant="outlined" color="primary" onClick={props.onClose}>
<Trans>Close</Trans>
</Button>
}
>
<Grid container spacing={2}>
<Grid item xs={12}>
<BoxText alignItems="left">
<ReactMarkdown components={renderers}>{$data}</ReactMarkdown>
</BoxText>
</Grid>
</Grid>
</Dialog>
);
}
Changelog.defaultProps = {
open: false,
current: '',
previous: '',
onClose: () => {},
};

View File

@ -10,8 +10,6 @@ import Button from '@mui/material/Button';
import ButtonBase from '@mui/material/ButtonBase'; import ButtonBase from '@mui/material/ButtonBase';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import DoNotDisturbAltIcon from '@mui/icons-material/DoNotDisturbAlt'; import DoNotDisturbAltIcon from '@mui/icons-material/DoNotDisturbAlt';
import FullscreenIcon from '@mui/icons-material/Fullscreen';
import FullscreenExitIcon from '@mui/icons-material/FullscreenExit';
import Grid from '@mui/material/Grid'; import Grid from '@mui/material/Grid';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import LensIcon from '@mui/icons-material/Lens'; import LensIcon from '@mui/icons-material/Lens';
@ -121,7 +119,7 @@ const ImageBackdrop = styled('span')(({ theme }) => ({
border: `2px solid ${theme.palette.primary.dark}`, border: `2px solid ${theme.palette.primary.dark}`,
})); }));
function ChannelButton(props, largeChannelList) { function ChannelButton(props) {
const classes = useStyles(); const classes = useStyles();
const theme = useTheme(); const theme = useTheme();
@ -156,7 +154,7 @@ function ChannelButton(props, largeChannelList) {
} }
return ( return (
<Grid item xs={12} sm={6} md={4} lg={3} style={{ paddingBottom: largeChannelList ? '10px' : 'auto' }}> <Grid item xs={12} sm={6} md={4} lg={3}>
<ImageButton focusRipple disabled={props.disabled} onClick={props.onClick} style={{ width: props.width }}> <ImageButton focusRipple disabled={props.disabled} onClick={props.onClick} style={{ width: props.width }}>
<Stack direction="column" spacing={0.5}> <Stack direction="column" spacing={0.5}>
<Image <Image
@ -194,21 +192,6 @@ ChannelButton.defaultProps = {
onClick: () => {}, onClick: () => {},
}; };
const calculateColumnsPerRow = (breakpointSmall, breakpointMedium, breakpointLarge) => {
if (breakpointLarge) {
return 4;
} else if (breakpointMedium) {
return 3;
} else if (breakpointSmall) {
return 2;
}
return 1;
};
const calculateRowsToFit = (windowHeight, thumbnailHeight, otherUIHeight) => {
return Math.floor((windowHeight - otherUIHeight) / thumbnailHeight);
};
export default function ChannelList(props) { export default function ChannelList(props) {
const classes = useStyles(); const classes = useStyles();
const theme = useTheme(); const theme = useTheme();
@ -222,12 +205,8 @@ export default function ChannelList(props) {
open: false, open: false,
name: '', name: '',
}); });
const [windowWidth, setWindowWidth] = React.useState(window.innerWidth);
const [windowHeight, setWindowHeight] = React.useState(window.innerHeight);
const { channels: allChannels, channelid, onClick, onClose, onState } = props; const { channels: allChannels, channelid, onClick, onState } = props;
const [$largeChannelList, setLargeChannelList] = React.useState(false);
React.useEffect(() => { React.useEffect(() => {
(async () => { (async () => {
@ -267,40 +246,19 @@ export default function ChannelList(props) {
disabled={channelid === channel.channelid} disabled={channelid === channel.channelid}
onClick={() => { onClick={() => {
onClick(channel.channelid); onClick(channel.channelid);
if ($largeChannelList) {
onClose();
}
}} }}
largeChannelList={$largeChannelList}
/> />
); );
}); });
setChannels(channels); setChannels(channels);
})(); })();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [$pos, allChannels, $nChannels, channelid, onClick, onState]); }, [$pos, allChannels, $nChannels, channelid, onClick, onState]);
const onMount = async () => { const onMount = async () => {
setPos(0); setPos(0);
}; };
React.useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
setWindowHeight(window.innerHeight);
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
React.useEffect(() => {
const columns = calculateColumnsPerRow(breakpointSmall, breakpointMedium, breakpointLarge);
const rows = $largeChannelList ? calculateRowsToFit(windowHeight, 200, 60) : 1;
setNChannels(rows * columns);
}, [breakpointSmall, breakpointMedium, breakpointLarge, windowHeight, windowWidth, $largeChannelList]);
const handleAddChannelDialog = () => { const handleAddChannelDialog = () => {
setAddChannel({ setAddChannel({
...$addChannel, ...$addChannel,
@ -324,11 +282,6 @@ export default function ChannelList(props) {
return null; return null;
} }
const handleLargeChannelList = () => {
setLargeChannelList(!$largeChannelList);
setPos(0);
};
return ( return (
<React.Fragment> <React.Fragment>
<SwipeableDrawer <SwipeableDrawer
@ -336,13 +289,7 @@ export default function ChannelList(props) {
open={props.open} open={props.open}
onOpen={() => {}} onOpen={() => {}}
onClose={props.onClose} onClose={props.onClose}
sx={{ sx={{ marginButtom: 60 }}
marginButtom: 60,
'& .MuiDrawer-paper': {
top: $largeChannelList ? '0px!important' : 'auto!important',
height: $largeChannelList ? '100vh' : 'auto',
},
}}
BackdropProps={{ invisible: true }} BackdropProps={{ invisible: true }}
classes={{ classes={{
paper: classes.drawerPaper, paper: classes.drawerPaper,
@ -360,9 +307,6 @@ export default function ChannelList(props) {
<IconButton color="inherit" size="large" onClick={handleAddChannelDialog}> <IconButton color="inherit" size="large" onClick={handleAddChannelDialog}>
<AddIcon /> <AddIcon />
</IconButton> </IconButton>
<IconButton color="inherit" size="large" onClick={handleLargeChannelList}>
{$largeChannelList ? <FullscreenExitIcon /> : <FullscreenIcon />}
</IconButton>
<IconButton color="inherit" size="large" onClick={props.onClose}> <IconButton color="inherit" size="large" onClick={props.onClose}>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
@ -383,7 +327,7 @@ export default function ChannelList(props) {
</IconButton> </IconButton>
</Stack> </Stack>
<Stack direction="row" spacing={0} className={classes.channelItems}> <Stack direction="row" spacing={0} className={classes.channelItems}>
<Grid container spacing={0} justifyContent={$largeChannelList ? 'flex-start' : 'center'}> <Grid container spacing={0} justifyContent="center">
{$channels} {$channels}
</Grid> </Grid>
</Stack> </Stack>

View File

@ -16,19 +16,9 @@ export default function EncodingSelect(props) {
const { i18n } = useLingui(); const { i18n } = useLingui();
const profile = props.profile; const profile = props.profile;
let availableEncoders = [];
let availableDecoders = [];
if (props.type === 'video') {
availableEncoders = props.skills.encoders.video;
availableDecoders = props.skills.decoders.video;
} else if (props.type === 'audio') {
availableEncoders = props.skills.encoders.audio;
}
const handleDecoderChange = (event) => { const handleDecoderChange = (event) => {
const decoder = profile.decoder; const decoder = profile.decoder;
const stream = props.streams[profile.stream];
decoder.coder = event.target.value; decoder.coder = event.target.value;
// If the coder changes, use the coder's default settings // If the coder changes, use the coder's default settings
@ -40,7 +30,7 @@ export default function EncodingSelect(props) {
} }
if (c !== null) { if (c !== null) {
const defaults = c.defaults(stream, props.skills); const defaults = c.defaults();
decoder.settings = defaults.settings; decoder.settings = defaults.settings;
decoder.mapping = defaults.mapping; decoder.mapping = defaults.mapping;
} }
@ -59,7 +49,6 @@ export default function EncodingSelect(props) {
const handleEncoderChange = (event) => { const handleEncoderChange = (event) => {
const encoder = profile.encoder; const encoder = profile.encoder;
const stream = props.streams[profile.stream];
encoder.coder = event.target.value; encoder.coder = event.target.value;
// If the coder changes, use the coder's default settings // If the coder changes, use the coder's default settings
@ -71,7 +60,7 @@ export default function EncodingSelect(props) {
} }
if (c !== null) { if (c !== null) {
const defaults = c.defaults(stream, props.skills); const defaults = c.defaults({});
encoder.settings = defaults.settings; encoder.settings = defaults.settings;
encoder.mapping = defaults.mapping; encoder.mapping = defaults.mapping;
} }
@ -127,10 +116,10 @@ export default function EncodingSelect(props) {
let encoderSettingsHelp = null; let encoderSettingsHelp = null;
let coder = encoderRegistry.Get(profile.encoder.coder); let coder = encoderRegistry.Get(profile.encoder.coder);
if (coder !== null && availableEncoders.includes(coder.coder)) { if (coder !== null && props.availableEncoders.includes(coder.coder)) {
const Settings = coder.component; const Settings = coder.component;
encoderSettings = <Settings stream={stream} settings={profile.encoder.settings} skills={props.skills} onChange={handleEncoderSettingsChange} />; encoderSettings = <Settings stream={stream} settings={profile.encoder.settings} onChange={handleEncoderSettingsChange} />;
if (props.type === 'video' && !['copy', 'none', 'rawvideo'].includes(coder.coder)) { if (props.type === 'video' && !['copy', 'none', 'rawvideo'].includes(coder.coder)) {
encoderSettingsHelp = handleEncoderHelp(coder.coder); encoderSettingsHelp = handleEncoderHelp(coder.coder);
@ -141,7 +130,7 @@ export default function EncodingSelect(props) {
for (let c of encoderRegistry.List()) { for (let c of encoderRegistry.List()) {
// Does ffmpeg support the coder? // Does ffmpeg support the coder?
if (!availableEncoders.includes(c.coder)) { if (!props.availableEncoders.includes(c.coder)) {
continue; continue;
} }
@ -155,14 +144,14 @@ export default function EncodingSelect(props) {
encoderList.push( encoderList.push(
<MenuItem value={c.coder} key={c.coder}> <MenuItem value={c.coder} key={c.coder}>
{c.name} {c.name}
</MenuItem>, </MenuItem>
); );
} }
} else { } else {
encoderList.push( encoderList.push(
<MenuItem value={c.coder} key={c.coder}> <MenuItem value={c.coder} key={c.coder}>
{c.name} {c.name}
</MenuItem>, </MenuItem>
); );
} }
} }
@ -184,24 +173,22 @@ export default function EncodingSelect(props) {
if (coder.coder !== 'copy' && coder.coder !== 'none') { if (coder.coder !== 'copy' && coder.coder !== 'none') {
let c = decoderRegistry.Get(profile.decoder.coder); let c = decoderRegistry.Get(profile.decoder.coder);
if (c !== null && availableDecoders.includes(c.coder)) { if (c !== null && props.availableDecoders.includes(c.coder)) {
const Settings = c.component; const Settings = c.component;
decoderSettings = <Settings stream={stream} settings={profile.decoder.settings} skills={props.skills} onChange={handleDecoderSettingsChange} />; decoderSettings = <Settings stream={stream} settings={profile.decoder.settings} onChange={handleDecoderSettingsChange} />;
} }
// List all decoders for the codec of the stream // List all decoders for the codec of the stream
for (let c of decoderRegistry.GetCodersForCodec(stream.codec, availableDecoders, 'any')) { for (let c of decoderRegistry.GetCodersForCodec(stream.codec, props.availableDecoders, 'any')) {
decoderList.push( decoderList.push(
<MenuItem value={c.coder} key={c.coder}> <MenuItem value={c.coder} key={c.coder}>
{c.name} {c.name}
</MenuItem>, </MenuItem>
); );
} }
} }
// TODO: in case there's no decoder for a codec it should be mentioned.
return ( return (
<Grid container spacing={2}> <Grid container spacing={2}>
<Grid item xs={12}> <Grid item xs={12}>
@ -250,6 +237,7 @@ EncodingSelect.defaultProps = {
streams: [], streams: [],
profile: {}, profile: {},
codecs: [], codecs: [],
skills: {}, availableEncoders: [],
onChange: function (encoder, decoder, automatic) {}, availableDecoders: [],
onChange: function (encoder, decoder) {},
}; };

View File

@ -1,28 +0,0 @@
import React from 'react';
// Adapted from https://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable-string
export default function Filesize(props) {
let bytes = props.bytes;
const thresh = props.si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}
const units = props.si ? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] : ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
let u = -1;
const r = 10 ** props.digits;
do {
bytes /= thresh;
++u;
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
return <React.Fragment>{bytes.toFixed(props.digits) + ' ' + units[u]}</React.Fragment>;
}
Filesize.defaultProps = {
bytes: 0,
si: false,
digits: 1,
};

View File

@ -1,134 +0,0 @@
import React from 'react';
import { Trans } from '@lingui/macro';
import Grid from '@mui/material/Grid';
import Typography from '@mui/material/Typography';
// Import all filters (audio/video)
import * as Filters from './filters';
// Import all encoders (audio/video)
import * as Encoders from './coders/Encoders';
export default function FilterSelect(props) {
const profile = props.profile;
// handleFilterChange
// what: Filter name
// settings (component settings): {Key: Value}
// mapping (FFmpeg -af/-vf args): ['String', ...]
const handleFilterSettingsChange = (what) => (settings, graph, automatic) => {
const filter = profile.filter;
// Store mapping/settings per component
filter.settings[what] = {
settings: settings,
graph: graph,
};
// Get the order of the filters
let filterOrder = [];
if (props.type === 'video') {
filterOrder = Filters.Video.Filters();
} else {
filterOrder = Filters.Audio.Filters();
}
// Create the filter graph in the order as the filters are registered
const graphs = [];
for (let f of filterOrder) {
if (!(f in filter.settings)) {
continue;
}
if (filter.settings[f].graph.length !== 0) {
graphs.push(filter.settings[f].graph);
}
}
filter.graph = graphs.join(',');
props.onChange(filter, automatic);
};
// Set filterRegistry by type
let filterRegistry = null;
if (props.type === 'video') {
filterRegistry = Filters.Video;
} else if (props.type === 'audio') {
filterRegistry = Filters.Audio;
} else {
return null;
}
// Checks the state of hwaccel (gpu encoding)
let encoderRegistry = null;
let hwaccel = false;
if (props.type === 'video') {
encoderRegistry = Encoders.Video;
for (let encoder of encoderRegistry.List()) {
if (encoder.codec === props.profile.encoder.coder && encoder.hwaccel) {
hwaccel = true;
}
}
}
// Creates filter components
let filterSettings = [];
if (!hwaccel) {
for (let c of filterRegistry.List()) {
// Checks FFmpeg skills (filter is available)
if (props.availableFilters.includes(c.filter)) {
const Settings = c.component;
if (!(c.filter in profile.filter.settings)) {
profile.filter.settings[c.filter] = c.defaults();
} else {
profile.filter.settings[c.filter] = {
...c.defaults(),
...profile.filter.settings[c.filter],
};
}
filterSettings.push(
<Settings key={c.filter} settings={profile.filter.settings[c.filter].settings} onChange={handleFilterSettingsChange(c.filter)} />,
);
}
}
}
// No suitable filter found
if (filterSettings === null && !hwaccel) {
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography>
<Trans>No suitable filter found.</Trans>
</Typography>
</Grid>
</Grid>
);
// hwaccel requires further settings
} else if (hwaccel) {
return false;
}
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<Typography>
<Trans>Select your filter settings (optional):</Trans>
</Typography>
</Grid>
{filterSettings}
</Grid>
);
}
FilterSelect.defaultProps = {
type: '',
profile: {},
availableFilters: [],
onChange: function (filter, automatic) {},
};

View File

@ -42,21 +42,14 @@ export default function LanguageSelect(props) {
return ( return (
<Select className={classes.root} variant="standard" displayEmpty value={i18n.locale} onChange={handleChange}> <Select className={classes.root} variant="standard" displayEmpty value={i18n.locale} onChange={handleChange}>
<MenuItem value="en">English</MenuItem> <MenuItem value="en">English </MenuItem>
<MenuItem value="da">Dansk</MenuItem> <MenuItem value="de">Deutsch </MenuItem>
<MenuItem value="de">Deutsch</MenuItem> <MenuItem value="es">Español </MenuItem>
<MenuItem value="el">Ελληνικά</MenuItem> <MenuItem value="fr">Français </MenuItem>
<MenuItem value="es">Español</MenuItem> <MenuItem value="it">Italiano </MenuItem>
<MenuItem value="fr">Français</MenuItem>
<MenuItem value="it">Italiano</MenuItem>
<MenuItem value="ko">한국어</MenuItem>
<MenuItem value="pl">Polski</MenuItem> <MenuItem value="pl">Polski</MenuItem>
<MenuItem value="pt-br">Português</MenuItem> <MenuItem value="pt">Português </MenuItem>
<MenuItem value="ru">Русский</MenuItem> <MenuItem value="ru">Русский </MenuItem>
<MenuItem value="sl">Slovenščina</MenuItem>
<MenuItem value="tr">Türkçe</MenuItem>
<MenuItem value="uk">Українська мова</MenuItem>
<MenuItem value="zh-hans">中文(简体)</MenuItem>
</Select> </Select>
); );
} }

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@ -15,9 +15,16 @@ const MenuProps = {
export default function Component(props) { export default function Component(props) {
return ( return (
<FormControl variant={props.variant} disabled={props.disabled} fullWidth> <FormControl variant="outlined" fullWidth>
<InputLabel>{props.label}</InputLabel> <InputLabel>{props.label}</InputLabel>
<Select multiple value={props.value} onChange={props.onChange} input={<OutlinedInput />} renderValue={props.renderValue} MenuProps={MenuProps}> <Select
multiple
value={props.value}
onChange={props.onChange}
input={<OutlinedInput />}
renderValue={(selected) => selected.join(', ')}
MenuProps={MenuProps}
>
{props.children} {props.children}
</Select> </Select>
</FormControl> </FormControl>
@ -25,10 +32,7 @@ export default function Component(props) {
} }
Component.defaultProps = { Component.defaultProps = {
variant: 'outlined',
label: '', label: '',
value: [], value: [],
disabled: false,
renderValue: (selected) => selected.join(', '),
onChange: function (event) {}, onChange: function (event) {},
}; };

View File

@ -53,8 +53,6 @@ export default function Password(props) {
endAdornment={adornment} endAdornment={adornment}
label={props.label} label={props.label}
autoComplete={props.autoComplete} autoComplete={props.autoComplete}
inputProps={props.inputProps}
error={props.error}
/> />
{props.helperText && <FormHelperText>{props.helperText}</FormHelperText>} {props.helperText && <FormHelperText>{props.helperText}</FormHelperText>}
</FormControl> </FormControl>
@ -69,8 +67,6 @@ Password.defaultProps = {
autoComplete: 'current-password', autoComplete: 'current-password',
env: false, env: false,
show: false, show: false,
helperText: false, helperText: null,
inputProps: {},
error: false,
onChange: function (value) {}, onChange: function (value) {},
}; };

101
src/misc/Player/clappr.js Normal file
View File

@ -0,0 +1,101 @@
import React from 'react';
import { Plugins } from '@clappr/plugins';
import Clappr from '@clappr/core';
import Grid from '@mui/material/Grid';
import HLS from '@clappr/hlsjs-playback';
//import ClapprStats from '@clappr/stats-plugin';
/*
import Clappr from 'clappr';
import ClapprStats from 'clappr-stats';
*/
//import ClapprNerdStats from 'clappr-nerd-stats';
Clappr.Loader.registerPlayback(HLS);
for (let plugin of Object.values(Plugins)) {
Clappr.Loader.registerPlugin(plugin);
}
class Player extends React.Component {
constructor(props) {
super(props);
this.player = new Clappr.Player({});
this.playerRef = React.createRef();
}
componentDidMount() {
this.player.attachTo(this.playerRef.current);
this.setConfig(this.props.config);
this.setSource(this.props.source);
}
componentWillUnmount() {
this.player.destroy();
}
shouldComponentUpdate(nextProps, _) {
if (nextProps.source !== this.props.source) {
this.setSource(nextProps.source);
}
return false;
}
setSource(source) {
this.player.load(source);
}
setConfig(config) {
delete config.source;
delete config.sources;
delete config.parent;
delete config.parentid;
config = {
plugins: [],
...config,
};
const plugins = config.plugins;
config.plugins = [];
for (let p of plugins) {
switch (p) {
/*
case 'ClapprStats':
config.plugins.push(ClapprStats);
break;
*/
/*
case 'ClapprNerdStats':
config.plugins.push(ClapprNerdStats);
break;
*/
default:
break;
}
}
this.player.configure(config);
}
render() {
return (
<Grid
container
direction="column"
justifyContent="center"
alignItems="center"
spacing={1}
style={{ position: 'absolute', top: 0, left: 0, bottom: 0, right: 0 }}
ref={this.playerRef}
></Grid>
);
}
}
export default Player;

View File

@ -1,9 +1,10 @@
import React from 'react'; import React from 'react';
import Clappr from './clappr';
import VideoJS from './videojs'; import VideoJS from './videojs';
export default function Player(props) { export default function Player(props) {
const type = props.type ? props.type : 'videojs-internal'; const type = props.type ? props.type : 'clappr';
if (type === 'videojs-internal' || type === 'videojs-public') { if (type === 'videojs-internal' || type === 'videojs-public') {
const config = { const config = {
@ -14,9 +15,6 @@ export default function Player(props) {
liveui: true, liveui: true,
responsive: true, responsive: true,
fluid: true, fluid: true,
plugins: {
reloadSourceOnError: {},
},
sources: [{ src: props.source, type: 'application/x-mpegURL' }], sources: [{ src: props.source, type: 'application/x-mpegURL' }],
}; };
@ -45,19 +43,17 @@ export default function Player(props) {
overlay = imgTag.outerHTML; overlay = imgTag.outerHTML;
} }
if (player.overlay) { player.overlay({
player.overlay({ align: props.logo.position,
align: props.logo.position, overlays: [
overlays: [ {
{ showBackground: false,
showBackground: false, content: overlay,
content: overlay, start: 'playing',
start: 'playing', end: 'pause',
end: 'pause', },
}, ],
], });
});
}
} }
if (props.autoplay === true) { if (props.autoplay === true) {
@ -80,6 +76,57 @@ export default function Player(props) {
}} }}
/> />
); );
} else {
const config = {
poster: props.poster,
autoPlay: props.autoplay,
mute: props.mute,
disableCanAutoPlay: true,
playback: {
playInline: true,
hlsjsConfig: {
enableWorker: false,
},
},
chromeless: !props.controls,
height: '100%',
width: '100%',
mediacontrol: {
seekbar: props.colors.seekbar,
buttons: props.colors.buttons,
},
};
if (props.logo.image.length !== 0) {
config.watermark = props.logo.image + '?' + Math.random();
config.position = props.logo.position;
if (props.logo.link.length !== 0) {
config.watermarkLink = props.logo.link;
}
}
if (props.ga.account.length !== 0) {
config.gaAccount = props.ga.account;
if (props.ga.name.length !== 0) {
config.gaTrackerName = props.ga.name;
}
}
if (props.statistics === true) {
config.plugins.push('ClapprStats', 'clapprNerdStats');
config.clapprStats = {
runEach: 1000,
onReport: (metrics) => {},
};
config.clapprNerdStats = {
shortcut: ['command+shift+s', 'ctrl+shift+s'],
iconPosition: 'top-right',
};
}
return <Clappr source={props.source} config={config} />;
} }
} }

View File

@ -89,7 +89,7 @@
} }
.vjs-internal .vjs-slider-horizontal .vjs-volume-level:before { .vjs-internal .vjs-slider-horizontal .vjs-volume-level:before {
top: 0em; top: -0.4em;
right: -0.5em; right: -0.5em;
} }

View File

@ -1 +1,98 @@
.vjs-internal{--video-js--primary:#eaea05}.vjs-internal .vjs-big-play-button{width:70px;height:70px;background:0 0;line-height:180px;font-size:180px;border:none;top:50%;left:50%;margin-top:-90px;color:rgba(255,255,255,.65)}.vjs-internal.vjs-big-play-button:focus,.vjs-internal:hover .vjs-big-play-button{background-color:transparent;color:rgba(255,255,255,1)}.vjs-internal .vjs-control-bar{top:calc(50% - 45px);height:100px;width:50px;background-color:#4d4d4d;border-top-right-radius:4px;border-bottom-right-radius:4px}.vjs-internal .vjs-button>.vjs-icon-placeholder:before{line-height:50px}.vjs-internal .vjs-play-progress:before{display:none}.vjs-internal .vjs-progress-control{display:none}.vjs-internal .vjs-paused,.vjs-internal .vjs-playing{width:50px;height:50px;border-radius:4px;background-color:#4d4d4d}.vjs-internal .vjs-volume-panel{width:50px;height:50px;border-radius:4px;padding-left:4px;background-color:#4d4d4d;margin:50px 0 0 -50px}.vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{padding-top:1em;transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,right 1s 1s,top 1s 1s}.vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-slider-active,.vjs-internal .vjs-volume-panel .vjs-volume-control:active,.vjs-internal .vjs-volume-panel.vjs-hover .vjs-mute-control~.vjs-volume-control,.vjs-internal .vjs-volume-panel.vjs-hover .vjs-volume-control,.vjs-internal .vjs-volume-panel:active .vjs-volume-control,.vjs-internal .vjs-volume-panel:focus .vjs-volume-control{visibility:visible;opacity:1;position:relative;transition:visibility .1s,opacity .1s,height .1s,width .1s,right 0s,top 0s}.vjs-internal .vjs-slider-horizontal .vjs-volume-level:before{top:0;right:-.5em}.vjs-internal .vjs-remaining-time{display:none}.vjs-internal .vjs-live-display{display:none}.vjs-internal .vjs-picture-in-picture-control{display:none}.vjs-internal .vjs-fullscreen-control{display:none}.vjs-internal .vjs-seek-to-live-control{display:none}.vjs-internal .vjs-subs-caps-button{display:none}.vjs-internal .vjs-custom-control-spacer{display:block;width:100%} .vjs-internal {
--video-js--primary: #eaea05;
}
.vjs-internal .vjs-big-play-button {
width: 70px;
height: 70px;
background: 0 0;
line-height: 180px;
font-size: 180px;
border: none;
top: 50%;
left: 50%;
margin-top: -90px;
color: rgba(255, 255, 255, 0.65);
}
.vjs-internal.vjs-big-play-button:focus,
.vjs-internal:hover .vjs-big-play-button {
background-color: transparent;
color: rgba(255, 255, 255, 1);
}
.vjs-internal .vjs-control-bar {
top: calc(50% - 45px);
height: 100px;
width: 50px;
background-color: #4d4d4d;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.vjs-internal .vjs-button > .vjs-icon-placeholder:before {
line-height: 50px;
}
.vjs-internal .vjs-play-progress:before {
display: none;
}
.vjs-internal .vjs-progress-control {
display: none;
}
.vjs-internal .vjs-paused,
.vjs-internal .vjs-playing {
width: 50px;
height: 50px;
border-radius: 4px;
background-color: #4d4d4d;
}
.vjs-internal .vjs-volume-panel {
width: 50px;
height: 50px;
border-radius: 4px;
padding-left: 4px;
background-color: #4d4d4d;
margin: 50px 0 0 -50px;
}
.vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
padding-top: 1em;
transition: visibility 1s, opacity 1s, height 1s 1s, width 1s, right 1s 1s,
top 1s 1s;
}
.vjs-internal .vjs-volume-panel .vjs-volume-control.vjs-slider-active,
.vjs-internal .vjs-volume-panel .vjs-volume-control:active,
.vjs-internal
.vjs-volume-panel.vjs-hover
.vjs-mute-control
~ .vjs-volume-control,
.vjs-internal .vjs-volume-panel.vjs-hover .vjs-volume-control,
.vjs-internal .vjs-volume-panel:active .vjs-volume-control,
.vjs-internal .vjs-volume-panel:focus .vjs-volume-control {
visibility: visible;
opacity: 1;
position: relative;
transition: visibility 0.1s, opacity 0.1s, height 0.1s, width 0.1s, right 0s,
top 0s;
}
.vjs-internal .vjs-slider-horizontal .vjs-volume-level:before {
top: -0.4em;
right: -0.5em;
}
.vjs-internal .vjs-remaining-time {
display: none;
}
.vjs-internal .vjs-live-display {
display: none;
}
.vjs-internal .vjs-picture-in-picture-control {
display: none;
}
.vjs-internal .vjs-fullscreen-control {
display: none;
}
.vjs-internal .vjs-seek-to-live-control {
display: none;
}
.vjs-internal .vjs-subs-caps-button {
display: none;
}
.vjs-internal .vjs-custom-control-spacer {
display: block;
width: 100%;
}

View File

@ -1,117 +1,117 @@
.vjs-public { .vjs-public {
--video-js--primary: #EAEA05; --video-js--primary: #eaea05;
} }
/* play btn */ /* play btn */
.vjs-public .vjs-big-play-button { .vjs-public .vjs-big-play-button {
width: 70px; width: 70px;
height: 70px; height: 70px;
background: none; background: none;
line-height: 180px; line-height: 180px;
font-size: 180px; font-size: 180px;
border: none; border: none;
top: 50%; top: 50%;
left: 50%; left: 50%;
margin-top: -90px; margin-top: -90px;
margin-left: -90px; margin-left: -90px;
color: rgba(255,255,255,.65); color: rgba(255, 255, 255, 0.65);
} }
.vjs-public:hover .vjs-big-play-button, .vjs-public:hover .vjs-big-play-button,
.vjs-public.vjs-big-play-button:focus { .vjs-public.vjs-big-play-button:focus {
background-color: transparent; background-color: transparent;
color: rgba(255,255,255,1); color: rgba(255, 255, 255, 1);
} }
/* controlbar */ /* controlbar */
.vjs-public .vjs-control-bar { .vjs-public .vjs-control-bar {
height: 70px; height: 70px;
padding-top: 20px; padding-top: 20px;
background: none; background: none;
background-image: linear-gradient(0deg, rgba(0,0,0,.85), transparent) background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.85), transparent);
} }
.vjs-public .vjs-time-tooltip { .vjs-public .vjs-time-tooltip {
z-index: 0; z-index: 0;
} }
.vjs-public .vjs-button>.vjs-icon-placeholder:before { .vjs-public .vjs-button > .vjs-icon-placeholder:before {
line-height: 50px line-height: 50px;
} }
/* progressbar */ /* progressbar */
.vjs-public .vjs-play-progress:before { .vjs-public .vjs-play-progress:before {
display: none display: none;
} }
.vjs-public .vjs-progress-control { .vjs-public .vjs-progress-control {
position: absolute; position: absolute;
top: 0; top: 0;
right: 0; right: 0;
left: 15px; left: 15px;
width: calc(100% - 30px); width: calc(100% - 30px);
height: 20px height: 20px;
} }
.vjs-public .vjs-progress-control .vjs-progress-holder { .vjs-public .vjs-progress-control .vjs-progress-holder {
position: absolute; position: absolute;
top: 20px; top: 20px;
right: 0; right: 0;
left: 0; left: 0;
width: 100%; width: 100%;
margin: 0 margin: 0;
} }
.vjs-public .vjs-play-progress { .vjs-public .vjs-play-progress {
background-color: var(--video-js--primary); background-color: var(--video-js--primary);
} }
.vjs-public .vjs-slider { .vjs-public .vjs-slider {
background: rgba(255,255,255,.25); background: rgba(255, 255, 255, 0.25);
} }
.vjs-public .vjs-load-progress { .vjs-public .vjs-load-progress {
background: rgba(255,255,255,.25); background: rgba(255, 255, 255, 0.25);
} }
.vjs-public .vjs-load-progress div { .vjs-public .vjs-load-progress div {
background: rgba(255,255,255,.25); background: rgba(255, 255, 255, 0.25);
} }
.vjs-public .vjs-remaining-time { .vjs-public .vjs-remaining-time {
order: 0; order: 0;
line-height: 50px; line-height: 50px;
flex: 3; flex: 3;
text-align: left; text-align: left;
} }
.vjs-public .vjs-live-control { .vjs-public .vjs-live-control {
line-height: 50px; line-height: 50px;
} }
/* volume-panel */ /* volume-panel */
.vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal { .vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
padding-top: .95em; padding-top: 1em;
} }
.vjs-public .vjs-control .vjs-volume-panel { .vjs-public .vjs-control .vjs-volume-panel {
width: 4.5em; width: 4.5em;
} }
/* live display */ /* live display */
.vjs-public .vjs-live-display { .vjs-public .vjs-live-display {
margin-left: 1.8em; margin-left: 1.8em;
} }
/* disable caps */ /* disable caps */
.vjs-public .vjs-subs-caps-button { .vjs-internal .vjs-subs-caps-button {
display: none!important; display: none;
} }
/* spacer */ /* spacer */
@ -123,52 +123,47 @@
/* overlay */ /* overlay */
.vjs-public .vjs-overlay > a > img {
width: 100%;
}
.vjs-public .vjs-overlay-no-background { .vjs-public .vjs-overlay-no-background {
max-width: 25%!important; max-width: 28% !important;
max-height: 28% !important;
} }
.vjs-public .vjs-overlay-no-background > img, .vjs-public .vjs-overlay-no-background > a > img {
max-width: 100%!important;
height: auto!important;
}
.vjs-public .vjs-overlay-top-left { .vjs-public .vjs-overlay-top-left {
top: 15px!important; top: 20px !important;
left: 20px!important; left: 30px !important;
text-align: right;
} }
.vjs-public .vjs-overlay-top-right { .vjs-public .vjs-overlay-top-right {
top: 15px!important; top: 20px !important;
right: 20px!important; right: 30px !important;
text-align: right;
} }
.vjs-public .vjs-overlay-bottom-left { .vjs-public .vjs-overlay-bottom-left {
bottom: 15px!important; bottom: 20px !important;
left: 20px!important; left: 30px !important;
text-align: left;
} }
.vjs-public .vjs-overlay-bottom-right { .vjs-public .vjs-overlay-bottom-right {
bottom: 15px!important; bottom: 20px !important;
right: 20px!important; right: 30px !important;
text-align: left;
} }
/* context menu */ /* context menu */
.vjs-public .vjs-license .vjs-menu .vjs-menu-content { .vjs-public .vjs-license .vjs-menu .vjs-menu-content {
background: rgba(0,0,0,.8); background: rgba(0, 0, 0, 0.8);
} }
.vjs-public .vjs-license-top-level-header { .vjs-public .vjs-license-top-level-header {
background: unset!important; background: unset !important;
border-bottom: 1px solid rgba(255,255,255,.25); border-bottom: 1px solid rgba(255, 255, 255, 0.25);
min-width: 100px;
} }
.vjs-public .vjs-lock-open { .vjs-public .vjs-lock-open {
z-index: 1000; z-index: 1000;
} }

View File

@ -1 +1,123 @@
.vjs-public{--video-js--primary:#EAEA05}.vjs-public .vjs-big-play-button{width:70px;height:70px;background:none;line-height:180px;font-size:180px;border:0;top:50%;left:50%;margin-top:-90px;margin-left:-90px;color:rgba(255,255,255,.65)}.vjs-public:hover .vjs-big-play-button,.vjs-public.vjs-big-play-button:focus{background-color:transparent;color:rgba(255,255,255,1)}.vjs-public .vjs-control-bar{height:70px;padding-top:20px;background:none;background-image:linear-gradient(0,rgba(0,0,0,.85),transparent)}.vjs-public .vjs-time-tooltip{z-index:0}.vjs-public .vjs-button>.vjs-icon-placeholder:before{line-height:50px}.vjs-public .vjs-play-progress:before{display:none}.vjs-public .vjs-progress-control{position:absolute;top:0;right:0;left:15px;width:calc(100% - 30px);height:20px}.vjs-public .vjs-progress-control .vjs-progress-holder{position:absolute;top:20px;right:0;left:0;width:100%;margin:0}.vjs-public .vjs-play-progress{background-color:var(--video-js--primary)}.vjs-public .vjs-slider{background:rgba(255,255,255,.25)}.vjs-public .vjs-load-progress{background:rgba(255,255,255,.25)}.vjs-public .vjs-load-progress div{background:rgba(255,255,255,.25)}.vjs-public .vjs-remaining-time{order:0;line-height:50px;flex:3;text-align:left}.vjs-public .vjs-live-control{line-height:50px}.vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{padding-top:.95em}.vjs-public .vjs-control .vjs-volume-panel{width:4.5em}.vjs-public .vjs-live-display{margin-left:1.8em}.vjs-public .vjs-subs-caps-button{display:none!important}.vjs-public .vjs-custom-control-spacer{display:block;width:100%}.vjs-public .vjs-overlay-no-background{max-width:25%!important}.vjs-public .vjs-overlay-no-background>img,.vjs-public .vjs-overlay-no-background>a>img{max-width:100%!important;height:auto!important}.vjs-public .vjs-overlay-top-left{top:15px!important;left:20px!important;text-align:right}.vjs-public .vjs-overlay-top-right{top:15px!important;right:20px!important;text-align:right}.vjs-public .vjs-overlay-bottom-left{bottom:15px!important;left:20px!important;text-align:left}.vjs-public .vjs-overlay-bottom-right{bottom:15px!important;right:20px!important;text-align:left}.vjs-public .vjs-license .vjs-menu .vjs-menu-content{background:rgba(0,0,0,.8)}.vjs-public .vjs-license-top-level-header{background:unset!important;border-bottom:1px solid rgba(255,255,255,.25)}.vjs-public .vjs-lock-open{z-index:1000} .vjs-public {
--video-js--primary: #eaea05;
}
.vjs-public .vjs-big-play-button {
width: 70px;
height: 70px;
background: 0 0;
line-height: 180px;
font-size: 180px;
border: none;
top: 50%;
left: 50%;
margin-top: -90px;
margin-left: -90px;
color: rgba(255, 255, 255, 0.65);
}
.vjs-public.vjs-big-play-button:focus,
.vjs-public:hover .vjs-big-play-button {
background-color: transparent;
color: rgba(255, 255, 255, 1);
}
.vjs-public .vjs-control-bar {
height: 70px;
padding-top: 20px;
background: 0 0;
background-image: linear-gradient(0deg, rgba(0, 0, 0, 0.85), transparent);
}
.vjs-public .vjs-time-tooltip {
z-index: 0;
}
.vjs-public .vjs-button > .vjs-icon-placeholder:before {
line-height: 50px;
}
.vjs-public .vjs-play-progress:before {
display: none;
}
.vjs-public .vjs-progress-control {
position: absolute;
top: 0;
right: 0;
left: 15px;
width: calc(100% - 30px);
height: 20px;
}
.vjs-public .vjs-progress-control .vjs-progress-holder {
position: absolute;
top: 20px;
right: 0;
left: 0;
width: 100%;
margin: 0;
}
.vjs-public .vjs-play-progress {
background-color: var(--video-js--primary);
}
.vjs-public .vjs-slider {
background: rgba(255, 255, 255, 0.25);
}
.vjs-public .vjs-load-progress {
background: rgba(255, 255, 255, 0.25);
}
.vjs-public .vjs-load-progress div {
background: rgba(255, 255, 255, 0.25);
}
.vjs-public .vjs-remaining-time {
order: 0;
line-height: 50px;
flex: 3;
text-align: left;
}
.vjs-public .vjs-live-control {
line-height: 50px;
}
.vjs-public .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal {
padding-top: 1em;
}
.vjs-public .vjs-control .vjs-volume-panel {
width: 4.5em;
}
.vjs-public .vjs-live-display {
margin-left: 1.8em;
}
.vjs-internal .vjs-subs-caps-button {
display: none;
}
.vjs-public .vjs-custom-control-spacer {
display: block;
width: 100%;
}
.vjs-public .vjs-overlay > a > img {
width: 100%;
}
.vjs-public .vjs-overlay-no-background {
max-width: 28% !important;
max-height: 28% !important;
}
.vjs-public .vjs-overlay-top-left {
top: 20px !important;
left: 30px !important;
}
.vjs-public .vjs-overlay-top-right {
top: 20px !important;
right: 30px !important;
}
.vjs-public .vjs-overlay-bottom-left {
bottom: 20px !important;
left: 30px !important;
}
.vjs-public .vjs-overlay-bottom-right {
bottom: 20px !important;
right: 30px !important;
}
.vjs-public .vjs-license .vjs-menu .vjs-menu-content {
background: rgba(0, 0, 0, 0.8);
}
.vjs-public .vjs-license-top-level-header {
background: unset !important;
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
min-width: 100px;
}
.vjs-public .vjs-lock-open {
z-index: 1000;
}

View File

@ -31,7 +31,6 @@ export default function VideoJS(props) {
player.addClass('vjs-internal'); player.addClass('vjs-internal');
} }
player.addClass('video-js'); player.addClass('video-js');
player.addClass('vjs-16-9');
} else { } else {
// you can update player here [update player through props] // you can update player here [update player through props]
// const player = playerRef.current; // const player = playerRef.current;
@ -58,7 +57,7 @@ export default function VideoJS(props) {
direction="column" direction="column"
justifyContent="center" justifyContent="center"
alignItems="center" alignItems="center"
spacing={2} spacing={1}
style={{ position: 'absolute', top: 0, left: 0, bottom: 0, right: 0 }} style={{ position: 'absolute', top: 0, left: 0, bottom: 0, right: 0 }}
> >
<div data-vjs-player> <div data-vjs-player>

Some files were not shown because too many files have changed in this diff Show More