VideoVariantForm.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. // This content populates the video variant modal, which is spawned from the variants table. This relies on the `dataState` prop fed in by the table.
  2. import React, { FC } from 'react';
  3. import { Popconfirm, Row, Col, Slider, Collapse, Typography, Alert, Button } from 'antd';
  4. import classNames from 'classnames';
  5. import dynamic from 'next/dynamic';
  6. import { FieldUpdaterFunc, VideoVariant, UpdateArgs } from '../../types/config-section';
  7. import { TextField } from './TextField';
  8. import {
  9. DEFAULT_VARIANT_STATE,
  10. VIDEO_VARIANT_SETTING_DEFAULTS,
  11. VIDEO_NAME_DEFAULTS,
  12. ENCODER_PRESET_SLIDER_MARKS,
  13. ENCODER_PRESET_TOOLTIPS,
  14. VIDEO_BITRATE_DEFAULTS,
  15. VIDEO_BITRATE_SLIDER_MARKS,
  16. FRAMERATE_SLIDER_MARKS,
  17. FRAMERATE_DEFAULTS,
  18. FRAMERATE_TOOLTIPS,
  19. } from '../../utils/config-constants';
  20. import { ToggleSwitch } from './ToggleSwitch';
  21. const { Panel } = Collapse;
  22. // Lazy loaded components
  23. const ExclamationCircleFilled = dynamic(() => import('@ant-design/icons/ExclamationCircleFilled'), {
  24. ssr: false,
  25. });
  26. export type VideoVariantFormProps = {
  27. dataState: VideoVariant;
  28. onUpdateField: FieldUpdaterFunc;
  29. };
  30. export const VideoVariantForm: FC<VideoVariantFormProps> = ({
  31. dataState = DEFAULT_VARIANT_STATE,
  32. onUpdateField,
  33. }) => {
  34. const videoPassthroughEnabled = dataState.videoPassthrough;
  35. const handleFramerateChange = (value: number) => {
  36. onUpdateField({ fieldName: 'framerate', value });
  37. };
  38. const handleVideoBitrateChange = (value: number) => {
  39. onUpdateField({ fieldName: 'videoBitrate', value });
  40. };
  41. const handleVideoCpuUsageLevelChange = (value: number) => {
  42. onUpdateField({ fieldName: 'cpuUsageLevel', value });
  43. };
  44. const handleScaledWidthChanged = (args: UpdateArgs) => {
  45. const value = Number(args.value);
  46. // eslint-disable-next-line no-restricted-globals
  47. if (isNaN(value)) {
  48. return;
  49. }
  50. onUpdateField({ fieldName: 'scaledWidth', value: value || 0 });
  51. };
  52. const handleScaledHeightChanged = (args: UpdateArgs) => {
  53. const value = Number(args.value);
  54. // eslint-disable-next-line no-restricted-globals
  55. if (isNaN(value)) {
  56. return;
  57. }
  58. onUpdateField({ fieldName: 'scaledHeight', value: value || 0 });
  59. };
  60. // Video passthrough handling
  61. const handleVideoPassConfirm = () => {
  62. onUpdateField({ fieldName: 'videoPassthrough', value: true });
  63. };
  64. // If passthrough is currently on, set it back to false on toggle.
  65. // Else let the Popconfirm turn it on.
  66. const handleVideoPassthroughToggle = (value: boolean) => {
  67. if (videoPassthroughEnabled) {
  68. onUpdateField({ fieldName: 'videoPassthrough', value });
  69. }
  70. };
  71. const handleNameChanged = (args: UpdateArgs) => {
  72. onUpdateField({ fieldName: 'name', value: args.value });
  73. };
  74. // Slider notes
  75. const selectedVideoBRnote = () => {
  76. if (videoPassthroughEnabled) {
  77. return 'Bitrate selection is disabled when Video Passthrough is enabled.';
  78. }
  79. let note = `${dataState.videoBitrate}${VIDEO_BITRATE_DEFAULTS.unit}`;
  80. if (dataState.videoBitrate < 2000) {
  81. note = `${note} - Good for low bandwidth environments.`;
  82. } else if (dataState.videoBitrate < 3500) {
  83. note = `${note} - Good for most bandwidth environments.`;
  84. } else {
  85. note = `${note} - Good for high bandwidth environments.`;
  86. }
  87. return note;
  88. };
  89. const selectedFramerateNote = () => {
  90. if (videoPassthroughEnabled) {
  91. return 'Framerate selection is disabled when Video Passthrough is enabled.';
  92. }
  93. return FRAMERATE_TOOLTIPS[dataState.framerate] || '';
  94. };
  95. const cpuUsageNote = () => {
  96. if (videoPassthroughEnabled) {
  97. return 'CPU usage selection is disabled when Video Passthrough is enabled.';
  98. }
  99. return ENCODER_PRESET_TOOLTIPS[dataState.cpuUsageLevel] || '';
  100. };
  101. const classes = classNames({
  102. 'config-variant-form': true,
  103. 'video-passthrough-enabled': videoPassthroughEnabled,
  104. });
  105. return (
  106. <div className={classes}>
  107. <div className="video-varient-alert">
  108. <Alert
  109. type="info"
  110. action={
  111. <a
  112. href="https://owncast.online/docs/video?source=admin"
  113. target="_blank"
  114. rel="noopener noreferrer"
  115. >
  116. <div className="video-varient-alert-button-container">
  117. <Button size="small" type="text" icon={<ExclamationCircleFilled />}>
  118. Read more about how each of these settings can impact the performance of your
  119. server.
  120. </Button>
  121. </div>
  122. </a>
  123. }
  124. />
  125. </div>
  126. {videoPassthroughEnabled && (
  127. <p className="passthrough-warning">
  128. NOTE: Video Passthrough for this output stream variant is <em>enabled</em>, disabling the
  129. below video encoding settings.
  130. </p>
  131. )}
  132. <Row gutter={16}>
  133. <Col xs={24} lg={{ span: 24, pull: 3 }} className="video-text-field-container">
  134. <TextField
  135. maxLength="10"
  136. {...VIDEO_NAME_DEFAULTS}
  137. value={dataState.name}
  138. onChange={handleNameChanged}
  139. />
  140. </Col>
  141. <Col sm={24} md={12}>
  142. <div className="form-module cpu-usage-container">
  143. <Typography.Title level={3}>CPU or GPU Utilization</Typography.Title>
  144. <p className="description">
  145. Reduce to improve server performance, or increase it to improve video quality.
  146. </p>
  147. <div className="segment-slider-container">
  148. <Slider
  149. tipFormatter={value => ENCODER_PRESET_TOOLTIPS[value]}
  150. onChange={handleVideoCpuUsageLevelChange}
  151. min={0}
  152. max={Object.keys(ENCODER_PRESET_SLIDER_MARKS).length - 1}
  153. marks={ENCODER_PRESET_SLIDER_MARKS}
  154. defaultValue={dataState.cpuUsageLevel}
  155. value={dataState.cpuUsageLevel}
  156. disabled={dataState.videoPassthrough}
  157. />
  158. <p className="selected-value-note">{cpuUsageNote()}</p>
  159. </div>
  160. <p className="read-more-subtext">
  161. This could mean GPU or CPU usage depending on your server environment.
  162. <br />
  163. <br />
  164. <a
  165. href="https://owncast.online/docs/video/?source=admin#cpu-usage"
  166. target="_blank"
  167. rel="noopener noreferrer"
  168. >
  169. Read more about hardware performance.
  170. </a>
  171. </p>
  172. </div>
  173. </Col>
  174. <Col sm={24} md={11} offset={1}>
  175. {/* VIDEO BITRATE FIELD */}
  176. <div
  177. className={`form-module bitrate-container ${
  178. dataState.videoPassthrough ? 'disabled' : ''
  179. }`}
  180. >
  181. <Typography.Title level={3}>Video Bitrate</Typography.Title>
  182. <p className="description">{VIDEO_BITRATE_DEFAULTS.tip}</p>
  183. <div className="segment-slider-container">
  184. <Slider
  185. tipFormatter={value => `${value} ${VIDEO_BITRATE_DEFAULTS.unit}`}
  186. disabled={dataState.videoPassthrough}
  187. defaultValue={dataState.videoBitrate}
  188. value={dataState.videoBitrate}
  189. onChange={handleVideoBitrateChange}
  190. step={VIDEO_BITRATE_DEFAULTS.incrementBy}
  191. min={VIDEO_BITRATE_DEFAULTS.min}
  192. max={VIDEO_BITRATE_DEFAULTS.max}
  193. marks={VIDEO_BITRATE_SLIDER_MARKS}
  194. />
  195. <p className="selected-value-note">{selectedVideoBRnote()}</p>
  196. </div>
  197. <p className="read-more-subtext">
  198. <a
  199. href="https://owncast.online/docs/video/?source=admin"
  200. target="_blank"
  201. rel="noopener noreferrer"
  202. >
  203. Read more about bitrates.
  204. </a>
  205. </p>
  206. </div>
  207. </Col>
  208. </Row>
  209. <Collapse className="advanced-settings">
  210. <Panel header="Advanced Settings" key="1">
  211. <Row gutter={16}>
  212. <Col sm={24} md={12}>
  213. <div className="form-module resolution-module">
  214. <Typography.Title level={3}>Resolution</Typography.Title>
  215. <p className="description">
  216. Resizing your content will take additional resources on your server. If you wish
  217. to optionally resize your content for this stream output then you should either
  218. set the width <strong>or</strong> the height to keep your aspect ratio. <br />
  219. <br />
  220. <a
  221. href="https://owncast.online/docs/video/?source=admin"
  222. target="_blank"
  223. rel="noopener noreferrer"
  224. >
  225. Read more about resolutions.
  226. </a>
  227. </p>
  228. <br />
  229. <TextField
  230. type="number"
  231. {...VIDEO_VARIANT_SETTING_DEFAULTS.scaledWidth}
  232. value={dataState.scaledWidth}
  233. onChange={handleScaledWidthChanged}
  234. disabled={dataState.videoPassthrough}
  235. />
  236. <TextField
  237. type="number"
  238. {...VIDEO_VARIANT_SETTING_DEFAULTS.scaledHeight}
  239. value={dataState.scaledHeight}
  240. onChange={handleScaledHeightChanged}
  241. disabled={dataState.videoPassthrough}
  242. />
  243. </div>
  244. </Col>
  245. <Col sm={24} md={12}>
  246. {/* VIDEO PASSTHROUGH FIELD */}
  247. <div className="form-module video-passthrough-module">
  248. <Typography.Title level={3}>Video Passthrough</Typography.Title>
  249. <div className="description">
  250. <p>
  251. Enabling video passthrough may allow for less hardware utilization, but may also
  252. make your stream <strong>unplayable</strong>.
  253. </p>
  254. <p>
  255. All other settings for this stream output will be disabled if passthrough is
  256. used.
  257. </p>
  258. <p>
  259. <a
  260. href="https://owncast.online/docs/video/?source=admin#video-passthrough"
  261. target="_blank"
  262. rel="noopener noreferrer"
  263. >
  264. Read the documentation before enabling, as it impacts your stream.
  265. </a>
  266. </p>
  267. </div>
  268. <div className="advanced-switch-container">
  269. <Popconfirm
  270. disabled={dataState.videoPassthrough === true}
  271. title="Did you read the documentation about video passthrough and understand the risks involved with enabling it?"
  272. icon={<ExclamationCircleFilled />}
  273. onConfirm={handleVideoPassConfirm}
  274. okText="Yes"
  275. cancelText="No"
  276. getPopupContainer={triggerNode => triggerNode}
  277. placement="topLeft"
  278. >
  279. {/* adding an <a> tag to force Popcofirm to register click on toggle */}
  280. {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
  281. <a href="#">
  282. <div className="advanced-description-switch-container">
  283. <div className="advanced-description-wrapper">
  284. <p>Use Video Passthrough?</p>
  285. </div>
  286. <ToggleSwitch
  287. label=""
  288. fieldName="video-passthrough"
  289. checked={dataState.videoPassthrough}
  290. onChange={handleVideoPassthroughToggle}
  291. />
  292. </div>
  293. </a>
  294. <p>*{VIDEO_VARIANT_SETTING_DEFAULTS.videoPassthrough.tip}</p>
  295. </Popconfirm>
  296. </div>
  297. </div>
  298. </Col>
  299. </Row>
  300. {/* FRAME RATE FIELD */}
  301. <div className="form-module frame-rate-module">
  302. <Typography.Title level={3}>Frame rate</Typography.Title>
  303. <p className="description">{FRAMERATE_DEFAULTS.tip}</p>
  304. <div className="segment-slider-container">
  305. <Slider
  306. tipFormatter={value => `${value} ${FRAMERATE_DEFAULTS.unit}`}
  307. defaultValue={dataState.framerate}
  308. value={dataState.framerate}
  309. onChange={handleFramerateChange}
  310. step={FRAMERATE_DEFAULTS.incrementBy}
  311. min={FRAMERATE_DEFAULTS.min}
  312. max={FRAMERATE_DEFAULTS.max}
  313. marks={FRAMERATE_SLIDER_MARKS}
  314. disabled={dataState.videoPassthrough}
  315. />
  316. <p className="selected-value-note">{selectedFramerateNote()}</p>
  317. </div>
  318. <p className="read-more-subtext">
  319. <a
  320. href="https://owncast.online/docs/video/?source=admin#framerate"
  321. target="_blank"
  322. rel="noopener noreferrer"
  323. >
  324. Read more about framerates.
  325. </a>
  326. </p>
  327. </div>
  328. </Panel>
  329. </Collapse>
  330. </div>
  331. );
  332. };