diff --git a/web/pages/components/config/constants.tsx b/web/pages/components/config/constants.tsx index 19108bf8a..5040fac8d 100644 --- a/web/pages/components/config/constants.tsx +++ b/web/pages/components/config/constants.tsx @@ -23,6 +23,8 @@ export const SUCCESS_STATES = { }, }; +// CONFIG API ENDPOINTS +export const API_VIDEO_VARIANTS = '/video/streamoutputvariants'; export async function postConfigUpdateToAPI(args: ApiPostArgs) { const { diff --git a/web/pages/components/config/video-variant-form.tsx b/web/pages/components/config/video-variant-form.tsx index a4262f8ee..9d78b714f 100644 --- a/web/pages/components/config/video-variant-form.tsx +++ b/web/pages/components/config/video-variant-form.tsx @@ -1,6 +1,7 @@ +// This content populates the video variant modal, which is spawned from the variants table. import React from 'react'; import { Slider, Select, Switch, Divider, Collapse } from 'antd'; -import { PRESETS, VideoVariant } from '../../../types/config-section'; +import { FieldUpdaterFunc, PRESET, VideoVariant } from '../../../types/config-section'; import { ENCODER_PRESETS, DEFAULT_VARIANT_STATE } from './constants'; import InfoTip from '../info-tip'; @@ -46,29 +47,31 @@ const VIDEO_VARIANT_DEFAULTS = { interface VideoVariantFormProps { dataState: VideoVariant; - onUpdateField: () => void; + onUpdateField: FieldUpdaterFunc; } 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 }); + onUpdateField({ fieldName: 'framerate', value }); }; const handleVideoBitrateChange = (value: number) => { - onUpdateField({ fieldname: 'videoBitrate', value }); + onUpdateField({ fieldName: 'videoBitrate', value }); }; const handleAudioBitrateChange = (value: number) => { - onUpdateField({ fieldname: 'audioBitrate', value }); + onUpdateField({ fieldName: 'audioBitrate', value }); }; - const handleEncoderPresetChange = (value: PRESETS) => { - onUpdateField({ fieldname: 'encoderPreset', value }); + const handleEncoderPresetChange = (value: PRESET) => { + onUpdateField({ fieldName: 'encoderPreset', value }); }; const handleAudioPassChange = (value: boolean) => { - onUpdateField({ fieldname: 'audioPassthrough', value }); + onUpdateField({ fieldName: 'audioPassthrough', value }); }; const handleVideoPassChange = (value: boolean) => { - onUpdateField({ fieldname: 'videoPassthrough', value }); + onUpdateField({ fieldName: 'videoPassthrough', value }); }; @@ -109,7 +112,10 @@ export default function VideoVariantForm({ dataState = DEFAULT_VARIANT_STATE, on Encoder Preset:

- { ENCODER_PRESETS.map(preset => (
- - diff --git a/web/pages/components/config/video-variants-table.tsx b/web/pages/components/config/video-variants-table.tsx index 3c38377f3..e7e4ec641 100644 --- a/web/pages/components/config/video-variants-table.tsx +++ b/web/pages/components/config/video-variants-table.tsx @@ -3,39 +3,102 @@ 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 { VideoVariant } from '../../../types/config-section'; +import { UpdateArgs, VideoVariant } from '../../../types/config-section'; import VideoVariantForm from './video-variant-form'; -import { DEFAULT_VARIANT_STATE } from './constants'; +import { API_VIDEO_VARIANTS, DEFAULT_VARIANT_STATE, SUCCESS_STATES, RESET_TIMEOUT,postConfigUpdateToAPI } from './constants'; 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); - const [dataState, setDataState] = useState(DEFAULT_VARIANT_STATE); - const { serverConfig } = serverStatusData || {}; + + // current data inside modal + const [modalDataState, setModalDataState] = useState(DEFAULT_VARIANT_STATE); + + const [submitStatus, setSubmitStatus] = useState(null); + const [submitStatusMessage, setSubmitStatusMessage] = useState(''); + + const { serverConfig, setFieldInConfigState } = serverStatusData || {}; const { videoSettings } = serverConfig || {}; const { videoQualityVariants } = videoSettings || {}; + + let resetTimer = null; + if (!videoSettings) { return null; } - const handleModalOk = () => { - setDisplayModal(false); - setEditId(-1); + const resetStates = () => { + setSubmitStatus(null); + setSubmitStatusMessage(''); + resetTimer = null; + clearTimeout(resetTimer); } + const handleModalCancel = () => { setDisplayModal(false); setEditId(-1); + setModalDataState(DEFAULT_VARIANT_STATE); + } + + // posts all the variants at once as an array obj + const postUpdateToAPI = async (postValue: any) => { + await postConfigUpdateToAPI({ + apiPath: API_VIDEO_VARIANTS, + data: { value: postValue }, + onSuccess: () => { + setFieldInConfigState({ fieldName: 'videoQualityVariants', value: postValue, path: 'videoSettings' }); + + // close modal + setModalProcessing(false); + handleModalCancel(); + + setSubmitStatus('success'); + setSubmitStatusMessage('Variants updated.'); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + }, + onError: (message: string) => { + setSubmitStatus('error'); + setSubmitStatusMessage(message); + resetTimer = setTimeout(resetStates, RESET_TIMEOUT); + }, + }); + }; + + // on Ok, send all of dataState to api + // show loading + // close modal when api is done + const handleModalOk = () => { + setModalProcessing(true); + + const postData = [ + ...videoQualityVariants, + ]; + if (editId === -1) { + postData.push(modalDataState); + } else { + postData.splice(editId, 1, modalDataState); + } + console.log("==== submit postData", postData) + + postUpdateToAPI(postData); } - const handleUpdateField = (fieldName: string, value: any) => { - setDataState({ - ...dataState, + const handleUpdateField = ({ fieldName, value }: UpdateArgs) => { + console.log("===update field", fieldName, value) + setModalDataState({ + ...modalDataState, [fieldName]: value, }); } + + const { + icon: newStatusIcon = null, + message: newStatusMessage = '', + } = SUCCESS_STATES[submitStatus] || {}; const videoQualityColumns: ColumnsType = [ { @@ -50,13 +113,7 @@ export default function CurrentVariantsTable() { render: (bitrate: number) => !bitrate ? "Same as source" : `${bitrate} kbps`, }, - // { - // title: "Framerate", - // dataIndex: "framerate", - // key: "framerate", - // render: (framerate: number) => - // !framerate ? "Same as source" : `${framerate} fps`, - // }, + { title: "Encoder preset", dataIndex: "encoderPreset", @@ -64,47 +121,38 @@ export default function CurrentVariantsTable() { render: (preset: string) => !preset ? "n/a" : preset, }, - // { - // title: "Audio bitrate", - // dataIndex: "audioBitrate", - // key: "audioBitrate", - // render: (bitrate: number) => - // !bitrate ? "Same as source" : `${bitrate} kbps`, - // }, - // { - // title: "Audio passthrough", - // dataIndex: "audioPassthrough", - // key: "audioPassthrough", - // render: (item: boolean) => item ? : , - // }, - // { - // title: "Video passthrough", - // dataIndex: "videoPassthrough", - // key: "audioPassthrough", - // render: (item: boolean) => item ? : , - // }, { title: '', dataIndex: '', key: 'edit', render: (data: VideoVariant) => ( - + ), }, ]; + const statusMessage = ( +
+ {newStatusIcon} {newStatusMessage} {submitStatusMessage} +
+ ); const videoQualityVariantData = videoQualityVariants.map((variant, index) => ({ key: index + 1, ...variant })); + // console.log("=========", { modalDataState }) return ( <> Current Variants + {statusMessage} + - - - - + + + {statusMessage} +
+ ); diff --git a/web/pages/config-video.tsx b/web/pages/config-video.tsx index f190dccf3..432699afe 100644 --- a/web/pages/config-video.tsx +++ b/web/pages/config-video.tsx @@ -43,6 +43,9 @@ export default function VideoConfig() { Video configuration Learn more about configuring Owncast <a href="https://owncast.online/docs/configuration">by visiting the documentation.</a> +
+ {JSON.stringify(videoSettings)} +
void; + export interface UpdateArgs { fieldName: string; - value: string; + value: any; path?: string; } @@ -50,11 +52,11 @@ export interface ConfigInstanceDetailsFields { } -export type PRESETS = 'fast' | 'faster' | 'veryfast' | 'superfast' | 'ultrafast'; +export type PRESET = 'fast' | 'faster' | 'veryfast' | 'superfast' | 'ultrafast'; export interface VideoVariant { key?: number; // unique identifier generated on client side just for ant table rendering - encoderPreset: PRESETS, + encoderPreset: PRESET, framerate: number; audioPassthrough: boolean;