From 73dd08467268c8289be1f1b2a3c0328a8c014e46 Mon Sep 17 00:00:00 2001 From: gingervitis Date: Sun, 17 Jan 2021 00:40:46 -0800 Subject: [PATCH] add segement slider editor --- web/pages/components/config/constants.tsx | 1 + .../config/video-segments-editor.tsx | 185 ++++++++++++++++++ .../components/config/video-variant-form.tsx | 3 - .../config/video-variants-table.tsx | 12 +- web/pages/config-video.tsx | 57 +++--- web/styles/config.scss | 37 +++- web/utils/server-status-context.tsx | 2 + 7 files changed, 254 insertions(+), 43 deletions(-) create mode 100644 web/pages/components/config/video-segments-editor.tsx diff --git a/web/pages/components/config/constants.tsx b/web/pages/components/config/constants.tsx index 5040fac8d..0290a2181 100644 --- a/web/pages/components/config/constants.tsx +++ b/web/pages/components/config/constants.tsx @@ -25,6 +25,7 @@ export const SUCCESS_STATES = { // CONFIG API ENDPOINTS export const API_VIDEO_VARIANTS = '/video/streamoutputvariants'; +export const API_VIDEO_SEGMENTS = '/video/segmentconfig'; export async function postConfigUpdateToAPI(args: ApiPostArgs) { const { diff --git a/web/pages/components/config/video-segments-editor.tsx b/web/pages/components/config/video-segments-editor.tsx new file mode 100644 index 000000000..e042eae84 --- /dev/null +++ b/web/pages/components/config/video-segments-editor.tsx @@ -0,0 +1,185 @@ +import React, { useContext, useState } from 'react'; +import { Typography, Slider, } from 'antd'; +import { ServerStatusContext } from '../../../utils/server-status-context'; +import { API_VIDEO_SEGMENTS, SUCCESS_STATES, RESET_TIMEOUT,postConfigUpdateToAPI } from './constants'; + +const { Title } = Typography; + +// numberOfSegments +// secondsPerSegment +/* + +2 segments, 2 seconds +3 segments, 3 seconds +3 segments, 4 seconds +4 segments, 4 seconds <- default +8 segments, 4 seconds +10 segments, 4 seconds + +Lowest latancy, less reliability +-> Highest latancy, higher reliability +*/ +const DEFAULT_OPTION = 3; + +const SLIDER_OPTIONS = [ + { + numberOfSegments: 2, + secondsPerSegment: 2, + }, + { + numberOfSegments: 3, + secondsPerSegment: 3, + }, + { + numberOfSegments: 3, + secondsPerSegment: 4, + }, + { + numberOfSegments: 4, + secondsPerSegment: 4, + }, + { + numberOfSegments: 8, + secondsPerSegment: 4, + }, + { + numberOfSegments: 10, + secondsPerSegment: 4, + }, +]; + +const SLIDER_MARKS = { + 0: '1', + 1: '2', + 2: '3', + 3: '4', + 4: '5', + 5: '6', +}; + +const SLIDER_COMMENTS = { + 0: 'Lowest latency, but least reliability', + 1: 'Low latency, some reliability', + 2: 'Lower latency, some reliability', + 3: 'Optimal latency and reliability (Default)', + 4: 'High latency, better reliability', + 5: 'Highest latency, higher reliability', +}; + +interface SegmentToolTipProps { + value: string; +} + +function SegmentToolTip({ value }: SegmentToolTipProps) { + return ( + {value} + ); +} + +function findSelectedOption(videoSettings) { + const { numberOfSegments, secondsPerSegment } = videoSettings; + let selected = DEFAULT_OPTION; + SLIDER_OPTIONS.map((option, index) => { + if ( + (option.numberOfSegments === numberOfSegments && + option.secondsPerSegment === secondsPerSegment) || + option.numberOfSegments === numberOfSegments + ) { + selected = index; + } + return option; + }); + return selected; +} + +export default function VideoSegmentsEditor() { + const [submitStatus, setSubmitStatus] = useState(null); + const [submitStatusMessage, setSubmitStatusMessage] = useState(''); + + const serverStatusData = useContext(ServerStatusContext); + const { serverConfig, setFieldInConfigState } = serverStatusData || {}; + const { videoSettings } = serverConfig || {}; + + let resetTimer = null; + + if (!videoSettings) { + return null; + } + + const selectedOption = findSelectedOption(videoSettings); + + const resetStates = () => { + setSubmitStatus(null); + setSubmitStatusMessage(''); + resetTimer = null; + clearTimeout(resetTimer); + } + + // posts all the variants at once as an array obj + const postUpdateToAPI = async (postValue: any) => { + await postConfigUpdateToAPI({ + apiPath: API_VIDEO_SEGMENTS, + data: { value: postValue }, + onSuccess: () => { + setFieldInConfigState({ + fieldName: 'numberOfSegments', + value: postValue.numberOfSegments, + path: 'videoSettings' + }); + setFieldInConfigState({ + fieldName: 'secondsPerSegment', + value: postValue.secondsPerSegment, + path: 'videoSettings', + }); + + setSubmitStatus('success'); + setSubmitStatusMessage('Variants updated.'); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + }, + onError: (message: string) => { + setSubmitStatus('error'); + setSubmitStatusMessage(message); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + }, + }); + }; + + const { + icon: newStatusIcon = null, + message: newStatusMessage = '', + } = SUCCESS_STATES[submitStatus] || {}; + + const statusMessage = ( +
+ {newStatusIcon} {newStatusMessage} {submitStatusMessage} +
+ ); + + const handleSegmentChange = value => { + const postData = SLIDER_OPTIONS[value]; + postUpdateToAPI(postData); + }; + + return ( +
+ Video Tolerance +

+ There are trade-offs when cosidering video latency and reliability. Blah blah .. better wording here needed. +

+

+
+ } + onChange={handleSegmentChange} + min={0} + max={SLIDER_OPTIONS.length - 1} + marks={SLIDER_MARKS} + defaultValue={DEFAULT_OPTION} + value={selectedOption} + /> +
+ {statusMessage} +
+ ); +} \ No newline at end of file diff --git a/web/pages/components/config/video-variant-form.tsx b/web/pages/components/config/video-variant-form.tsx index 9d78b714f..75bc7d6a4 100644 --- a/web/pages/components/config/video-variant-form.tsx +++ b/web/pages/components/config/video-variant-form.tsx @@ -51,9 +51,6 @@ interface VideoVariantFormProps { } export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, onUpdateField }: VideoVariantFormProps) { - console.log("======form", dataState) - - // const [dataState, setDataState] = useState(initialValues); const handleFramerateChange = (value: number) => { onUpdateField({ fieldName: 'framerate', value }); diff --git a/web/pages/components/config/video-variants-table.tsx b/web/pages/components/config/video-variants-table.tsx index e7e4ec641..22b9fd3f2 100644 --- a/web/pages/components/config/video-variants-table.tsx +++ b/web/pages/components/config/video-variants-table.tsx @@ -1,6 +1,8 @@ -import React, { useContext, useState, useEffect } from 'react'; +// Updating a variant will post ALL the variants in an array as an update to the API. + +// todo : add DELETE option +import React, { useContext, useState } from 'react'; import { Typography, Table, Modal, Button } from 'antd'; -import { CheckOutlined, CloseOutlined } from '@ant-design/icons'; import { ColumnsType } from 'antd/lib/table'; import { ServerStatusContext } from '../../../utils/server-status-context'; import { UpdateArgs, VideoVariant } from '../../../types/config-section'; @@ -10,7 +12,6 @@ import { API_VIDEO_VARIANTS, DEFAULT_VARIANT_STATE, SUCCESS_STATES, RESET_TIMEOU const { Title } = Typography; export default function CurrentVariantsTable() { - const serverStatusData = useContext(ServerStatusContext); const [displayModal, setDisplayModal] = useState(false); const [modalProcessing, setModalProcessing] = useState(false); const [editId, setEditId] = useState(0); @@ -21,6 +22,7 @@ export default function CurrentVariantsTable() { const [submitStatus, setSubmitStatus] = useState(null); const [submitStatusMessage, setSubmitStatusMessage] = useState(''); + const serverStatusData = useContext(ServerStatusContext); const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { videoSettings } = serverConfig || {}; const { videoQualityVariants } = videoSettings || {}; @@ -82,13 +84,10 @@ export default function CurrentVariantsTable() { } else { postData.splice(editId, 1, modalDataState); } - console.log("==== submit postData", postData) - postUpdateToAPI(postData); } const handleUpdateField = ({ fieldName, value }: UpdateArgs) => { - console.log("===update field", fieldName, value) setModalDataState({ ...modalDataState, [fieldName]: value, @@ -146,7 +145,6 @@ export default function CurrentVariantsTable() { const videoQualityVariantData = videoQualityVariants.map((variant, index) => ({ key: index + 1, ...variant })); - // console.log("=========", { modalDataState }) return ( <> Current Variants diff --git a/web/pages/config-video.tsx b/web/pages/config-video.tsx index 432699afe..23a0be0b5 100644 --- a/web/pages/config-video.tsx +++ b/web/pages/config-video.tsx @@ -5,48 +5,53 @@ import { ServerStatusContext } from '../utils/server-status-context'; import VideoVariantsTable from './components/config/video-variants-table'; import TextField, { TEXTFIELD_TYPE_NUMBER } from './components/config/form-textfield'; import { TEXTFIELD_DEFAULTS } from './components/config/constants'; +import VideoSegmentsEditor from './components/config/video-segments-editor'; const { Title } = Typography; export default function VideoConfig() { - const [form] = Form.useForm(); + // const [form] = Form.useForm(); const serverStatusData = useContext(ServerStatusContext); - // const { serverConfig } = serverStatusData || {}; - // const { videoSettings } = serverConfig || {}; + const { serverConfig } = serverStatusData || {}; + const { videoSettings } = serverConfig || {}; // const { numberOfPlaylistItems, segmentLengthSeconds } = videoSettings || {}; - const videoSettings = serverStatusData?.serverConfig?.videoSettings; - const { numberOfPlaylistItems, segmentLengthSeconds } = videoSettings || {}; - const initialValues = { - numberOfPlaylistItems, - segmentLengthSeconds, - }; + // const videoSettings = serverStatusData?.serverConfig?.videoSettings; + // const { numberOfPlaylistItems, segmentLengthSeconds } = videoSettings || {}; + // const initialValues = { + // numberOfPlaylistItems, + // segmentLengthSeconds, + // }; - useEffect(() => { - form.setFieldsValue(initialValues); - }, [serverStatusData]); + // useEffect(() => { + // form.setFieldsValue(initialValues); + // }, [serverStatusData]); - const handleResetValue = (fieldName: string) => { - const defaultValue = TEXTFIELD_DEFAULTS.videoSettings[fieldName] && TEXTFIELD_DEFAULTS.videoSettings[fieldName].defaultValue || ''; + // const handleResetValue = (fieldName: string) => { + // const defaultValue = TEXTFIELD_DEFAULTS.videoSettings[fieldName] && TEXTFIELD_DEFAULTS.videoSettings[fieldName].defaultValue || ''; - form.setFieldsValue({ [fieldName]: initialValues[fieldName] || defaultValue }); - } + // form.setFieldsValue({ [fieldName]: initialValues[fieldName] || defaultValue }); + // } - const extraProps = { - handleResetValue, - initialValues: videoSettings, - configPath: 'videoSettings', - }; + // const extraProps = { + // handleResetValue, + // initialValues: videoSettings, + // configPath: 'videoSettings', + // }; return (
Video configuration - Learn more about configuring Owncast <a href="https://owncast.online/docs/configuration">by visiting the documentation.</a> +

Learn more about configuring Owncast by visiting the documentation.

-
+ {/*
{JSON.stringify(videoSettings)} -
-
+
*/} + + + +

+ {/*
-
+
*/}
diff --git a/web/styles/config.scss b/web/styles/config.scss index 50bbed418..6565b8641 100644 --- a/web/styles/config.scss +++ b/web/styles/config.scss @@ -19,6 +19,20 @@ } } +.module-container { + border-radius: 1em; + background-color: rgba(128,99,255,.1); + padding: 1.5em; + margin-bottom: 1em; +} + +.ant-slider-with-marks { + margin-right: 2em; +} +.ant-slider-mark-text { + font-size: .85em; + white-space: nowrap; +} .status-message { margin: 1rem 0; @@ -226,13 +240,6 @@ } .form-component { width: 60%; - .ant-slider-with-marks { - margin-right: 2em; - } - .ant-slider-mark-text { - font-size: .85em; - white-space: nowrap; - } .selected-value-note { font-size: .85em; @@ -255,3 +262,19 @@ background-color: rgba(0,0,0,.1); } } + +.config-video-segements-conatiner { + .segment-slider { + width: 90%; + margin: auto; + } + .status-message { + text-align: center; + } +} +.segment-tip { + width: 10em; + text-align: center; + margin: auto; + display: inline-block; +} \ No newline at end of file diff --git a/web/utils/server-status-context.tsx b/web/utils/server-status-context.tsx index 1b0ef152c..a5d329c09 100644 --- a/web/utils/server-status-context.tsx +++ b/web/utils/server-status-context.tsx @@ -1,3 +1,5 @@ +// TODO: add a notication after updating info that changes will take place either on a new stream or server restart. may be different for each field. + import React, { useState, useEffect } from 'react'; import PropTypes, { any } from 'prop-types';