diff --git a/web/.github/workflows/prettier.yml b/web/.github/workflows/prettier.yml index 22bc9db46..c39705559 100644 --- a/web/.github/workflows/prettier.yml +++ b/web/.github/workflows/prettier.yml @@ -23,7 +23,7 @@ jobs: uses: creyD/prettier_action@v3.1 with: # This part is also where you can pass other options, for example: - prettier_options: --write **/*.{js,tsx,jsx} + prettier_options: --write **/*.{js,tsx,jsx,css,scss} only_changed: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/web/components/config/edit-server-details.tsx b/web/components/config/edit-server-details.tsx index afe2223ee..6bbecfc9d 100644 --- a/web/components/config/edit-server-details.tsx +++ b/web/components/config/edit-server-details.tsx @@ -1,6 +1,7 @@ import React, { useState, useContext, useEffect } from 'react'; -import { Button, Tooltip } from 'antd'; +import { Button, Tooltip, Collapse, Popconfirm } from 'antd'; import { CopyOutlined, RedoOutlined } from '@ant-design/icons'; +const { Panel } = Collapse; import { TEXTFIELD_TYPE_NUMBER, TEXTFIELD_TYPE_PASSWORD } from './form-textfield'; import TextFieldWithSubmit from './form-textfield-with-submit'; @@ -14,6 +15,7 @@ import { TEXTFIELD_PROPS_STREAM_KEY, TEXTFIELD_PROPS_WEB_PORT, } from '../../utils/config-constants'; +import { fetchData, API_YP_RESET } from '../../utils/apis'; import { UpdateArgs } from '../../types/config-section'; @@ -24,7 +26,7 @@ export default function EditInstanceDetails() { const { serverConfig } = serverStatusData || {}; - const { streamKey, ffmpegPath, rtmpServerPort, webServerPort } = serverConfig; + const { streamKey, ffmpegPath, rtmpServerPort, webServerPort, yp } = serverConfig; const [copyIsVisible, setCopyVisible] = useState(false); @@ -66,6 +68,41 @@ export default function EditInstanceDetails() { } }; + const resetDirectoryRegistration = async () => { + try { + await fetchData(API_YP_RESET); + setMessage(''); + } catch (error) { + alert(error); + } + }; + + function ResetYP() { + // TODO: Uncomment this after it's styled. + // if (yp.enabled) { + return ( +
+ Reset Directory: + + + +

+ If you are experiencing issues with your listing on the Owncast Directory and were asked + to "reset" your connection to the service, you can do that here. The next time you go live + it will try and re-register your server with the directory from scratch. +

+
+ ); + // } + // return null; + } + function generateStreamKey() { let key = ''; for (let i = 0; i < 3; i += 1) { @@ -135,6 +172,13 @@ export default function EditInstanceDetails() { onChange={handleFieldChange} onSubmit={showConfigurationRestartMessage} /> + + +
+ +
+
+
); } diff --git a/web/components/config/video-variant-form.tsx b/web/components/config/video-variant-form.tsx index 1d85253a4..0d850f618 100644 --- a/web/components/config/video-variant-form.tsx +++ b/web/components/config/video-variant-form.tsx @@ -1,7 +1,8 @@ // This content populates the video variant modal, which is spawned from the variants table. import React from 'react'; import { Slider, Switch, Collapse } from 'antd'; -import { FieldUpdaterFunc, VideoVariant } from '../../types/config-section'; +import { FieldUpdaterFunc, VideoVariant, UpdateArgs } from '../../types/config-section'; +import TextField from './form-textfield'; import { DEFAULT_VARIANT_STATE } from '../../utils/config-constants'; import InfoTip from '../info-tip'; import CPUUsageSelector from './cpu-usage'; @@ -39,6 +40,20 @@ const VIDEO_VARIANT_DEFAULTS = { audioPassthrough: { tip: 'If No is selected, then you should set your desired Audio Bitrate.', }, + scaledWidth: { + fieldName: 'scaledWidth', + label: 'Resized Width', + maxLength: 4, + placeholder: '1080', + tip: "Optionally resize this content's width.", + }, + scaledHeight: { + fieldName: 'scaledHeight', + label: 'Resized Height', + maxLength: 4, + placeholder: '720', + tip: "Optionally resize this content's height.", + }, }; interface VideoVariantFormProps { @@ -62,7 +77,21 @@ export default function VideoVariantForm({ const handleVideoCpuUsageLevelChange = (value: number) => { onUpdateField({ fieldName: 'cpuUsageLevel', value }); }; + const handleScaledWidthChanged = (args: UpdateArgs) => { + const value = Number(args.value); + if (!isNaN(value)) { + return; + } + onUpdateField({ fieldName: 'scaledWidth', value: value }); + }; + const handleScaledHeightChanged = (args: UpdateArgs) => { + const value = Number(args.value); + if (!isNaN(value)) { + return; + } + onUpdateField({ fieldName: 'scaledHeight', value: value }); + }; const framerateDefaults = VIDEO_VARIANT_DEFAULTS.framerate; const framerateMin = framerateDefaults.min; const framerateMax = framerateDefaults.max; @@ -145,14 +174,29 @@ export default function VideoVariantForm({ -
-
-
-
- -
Touch if you dare.
+
+ Resizing your content will take additional resources on your server. If you wish to + optionally resize your output for this stream variant then you should either set the + width or the height to keep your aspect ratio. +
+
+ +
+
+ +
{/* FRAME RATE FIELD */}
diff --git a/web/types/config-section.ts b/web/types/config-section.ts index e85036397..05484c9df 100644 --- a/web/types/config-section.ts +++ b/web/types/config-section.ts @@ -56,6 +56,9 @@ export interface VideoVariant { audioBitrate: number; videoPassthrough: boolean; videoBitrate: number; + + scaledWidth: number; + scaledHeight: number; } export interface VideoSettingsFields { latencyLevel: number; diff --git a/web/utils/apis.ts b/web/utils/apis.ts index 1972f5745..83e193c0c 100644 --- a/web/utils/apis.ts +++ b/web/utils/apis.ts @@ -63,6 +63,8 @@ export const CREATE_WEBHOOK = `${API_LOCATION}webhooks/create`; // hard coded social icons list export const SOCIAL_PLATFORMS_LIST = `${NEXT_PUBLIC_API_HOST}api/socialplatforms`; +export const API_YP_RESET = `${API_LOCATION}yp/reset`; + export const TEMP_UPDATER_API = LOGS_ALL; const GITHUB_RELEASE_URL = 'https://api.github.com/repos/owncast/owncast/releases/latest'; @@ -71,14 +73,10 @@ interface FetchOptions { data?: any; method?: string; auth?: boolean; -}; +} export async function fetchData(url: string, options?: FetchOptions) { - const { - data, - method = 'GET', - auth = true, - } = options || {}; + const { data, method = 'GET', auth = true } = options || {}; const requestOptions: RequestInit = { method, @@ -114,7 +112,6 @@ export async function fetchData(url: string, options?: FetchOptions) { return {}; } - export async function getGithubRelease() { try { const response = await fetch(GITHUB_RELEASE_URL); @@ -133,29 +130,25 @@ export async function getGithubRelease() { // Stolen from https://gist.github.com/prenagha/98bbb03e27163bc2f5e4 const VPAT = /^\d+(\.\d+){0,2}$/; function upToDate(local, remote) { - if (!local || !remote || local.length === 0 || remote.length === 0) - return false; - if (local === remote) - return true; - if (VPAT.test(local) && VPAT.test(remote)) { - const lparts = local.split('.'); - while(lparts.length < 3) - lparts.push("0"); - const rparts = remote.split('.'); - while (rparts.length < 3) - rparts.push("0"); - // eslint-disable-next-line no-plusplus - for (let i=0; i<3; i++) { - const l = parseInt(lparts[i], 10); - const r = parseInt(rparts[i], 10); - if (l === r) - // eslint-disable-next-line no-continue - continue; - return l > r; - } - return true; - } - return local >= remote; + if (!local || !remote || local.length === 0 || remote.length === 0) return false; + if (local === remote) return true; + if (VPAT.test(local) && VPAT.test(remote)) { + const lparts = local.split('.'); + while (lparts.length < 3) lparts.push('0'); + const rparts = remote.split('.'); + while (rparts.length < 3) rparts.push('0'); + // eslint-disable-next-line no-plusplus + for (let i = 0; i < 3; i++) { + const l = parseInt(lparts[i], 10); + const r = parseInt(rparts[i], 10); + if (l === r) + // eslint-disable-next-line no-continue + continue; + return l > r; + } + return true; + } + return local >= remote; } // Make a request to the server status API and the Github releases API @@ -165,12 +158,12 @@ export async function upgradeVersionAvailable(currentVersion) { let recentReleaseVersion = recentRelease.tag_name; if (recentReleaseVersion.substr(0, 1) === 'v') { - recentReleaseVersion = recentReleaseVersion.substr(1) + recentReleaseVersion = recentReleaseVersion.substr(1); } if (!upToDate(currentVersion, recentReleaseVersion)) { - return recentReleaseVersion + return recentReleaseVersion; } return null; -} \ No newline at end of file +} diff --git a/web/utils/config-constants.tsx b/web/utils/config-constants.tsx index 88223e727..bbca05014 100644 --- a/web/utils/config-constants.tsx +++ b/web/utils/config-constants.tsx @@ -168,6 +168,8 @@ export const DEFAULT_VARIANT_STATE: VideoVariant = { audioPassthrough: true, // if false, then CAN set audiobitrate audioBitrate: 0, cpuUsageLevel: 3, + scaledHeight: null, + scaledWidth: null, }; export const DEFAULT_SOCIAL_HANDLE: SocialHandle = { diff --git a/web/utils/urls.ts b/web/utils/urls.ts index 058bf0639..b03a978f6 100644 --- a/web/utils/urls.ts +++ b/web/utils/urls.ts @@ -1,4 +1,4 @@ export function isValidUrl(url: string): boolean { - const pattern = /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/; + const pattern = /^(?:(?:https?|ftp):\/\/)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:\/\S*)?$/i; return !!pattern.test(url); -} \ No newline at end of file +}