diff --git a/CHANGELOG.md b/CHANGELOG.md
index 97cdb5e..6746981 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
## v1.8.0 > vx.x.x
- Mod uses placeholders for ingress setups ([#560](https://github.com/datarhei/restreamer-ui/issues/560))
+- Add frame interpolation (framerate) filter
## v1.7.0 > v1.8.0
diff --git a/src/misc/filters/index.js b/src/misc/filters/index.js
index 728982d..fe4b284 100644
--- a/src/misc/filters/index.js
+++ b/src/misc/filters/index.js
@@ -6,6 +6,7 @@ import * as Loudnorm from './audio/Loudnorm';
// Video Filter
import * as Bwdif from './video/Bwdif';
+import * as Framerate from './video/Framerate';
import * as Scale from './video/Scale';
import * as Transpose from './video/Transpose';
import * as HFlip from './video/HFlip';
@@ -54,6 +55,7 @@ audioRegistry.Register(Loudnorm);
// Video Filters
const videoRegistry = new Registry('video');
videoRegistry.Register(Bwdif);
+videoRegistry.Register(Framerate);
videoRegistry.Register(Scale);
videoRegistry.Register(Transpose);
videoRegistry.Register(HFlip);
diff --git a/src/misc/filters/video/Framerate.js b/src/misc/filters/video/Framerate.js
new file mode 100644
index 0000000..0243e19
--- /dev/null
+++ b/src/misc/filters/video/Framerate.js
@@ -0,0 +1,143 @@
+import React from 'react';
+
+import { useLingui } from '@lingui/react';
+import { Trans, t } from '@lingui/macro';
+import Grid from '@mui/material/Grid';
+
+import Checkbox from '../../Checkbox';
+import SelectCustom from '../../../misc/SelectCustom';
+
+// Framerate Filter
+// http://ffmpeg.org/ffmpeg-all.html#framerate
+
+function init(initialState) {
+ const state = {
+ enabled: false,
+ fps: '30',
+ ...initialState,
+ };
+
+ return state;
+}
+
+function createGraph(settings) {
+ settings = init(settings);
+
+ const mapping = [];
+
+ if (settings.enabled) {
+ mapping.push(`framerate=fps=${settings.fps}`);
+ }
+
+ return mapping.join(',');
+}
+
+function Framerate(props) {
+ const { i18n } = useLingui();
+ const sizes = [
+ { value: '60', label: '60' },
+ { value: '59.94', label: '59.94' },
+ { value: '50', label: '50' },
+ { value: '30', label: '30' },
+ { value: '29.97', label: '29.97 (NTSC)' },
+ { value: '25', label: '25 (PAL)' },
+ { value: '24', label: '24 (Film)' },
+ { value: '23.97', label: '23.97 (NTSC Film)' },
+ { value: '15', label: '15' },
+ { value: '10', label: '10' },
+ { value: 'custom', label: i18n._(t`Custom ...`) },
+ ];
+
+ return (
+
+ );
+}
+
+Framerate.defaultProps = {
+ label: Framerate,
+ customLabel: Custom framerate,
+ value: '',
+ variant: 'outlined',
+ allowCustom: true,
+ onChange: function (event) {},
+};
+
+function Filter(props) {
+ const settings = init(props.settings);
+
+ const handleChange = (newSettings) => {
+ let automatic = false;
+ if (!newSettings) {
+ newSettings = settings;
+ automatic = true;
+ }
+
+ props.onChange(newSettings, createGraph(newSettings), automatic);
+ };
+
+ const update = (what) => (event) => {
+ const newSettings = {
+ ...settings,
+ };
+ if (['enabled'].includes(what)) {
+ newSettings[what] = !settings.enabled;
+ } else {
+ newSettings[what] = event.target.value;
+ }
+
+ handleChange(newSettings);
+ };
+
+ React.useEffect(() => {
+ handleChange(null);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+
+
+ Framerate conversion (frame interpolation)} checked={settings.enabled} onChange={update('enabled')} />
+
+ {settings.enabled && (
+
+
+
+
+
+ )}
+
+ );
+}
+
+Filter.defaultProps = {
+ settings: {},
+ onChange: function (settings, mapping) {},
+};
+
+const filter = 'fps';
+const name = 'Frame Interpolation';
+const type = 'video';
+const hwaccel = false;
+
+function summarize(settings) {
+ return `${name} (${settings.fps}fps)`;
+}
+
+function defaults() {
+ const settings = init({});
+
+ return {
+ settings: settings,
+ graph: createGraph(settings),
+ };
+}
+
+export { name, filter, type, hwaccel, summarize, defaults, createGraph, Filter as component };