diff --git a/config/install/videojs_filter.settings.yml b/config/install/videojs_filter.settings.yml
new file mode 100644
index 0000000..dd4914f
--- /dev/null
+++ b/config/install/videojs_filter.settings.yml
@@ -0,0 +1,5 @@
+video_directory: ''
+poster_directory: ''
+poster_file: ''
+width: 320
+height: 240
diff --git a/config/schema/videosjs_filter.schema.yml b/config/schema/videosjs_filter.schema.yml
new file mode 100644
index 0000000..4206f77
--- /dev/null
+++ b/config/schema/videosjs_filter.schema.yml
@@ -0,0 +1,19 @@
+videojs_filter.settings:
+  type: mapping
+  label: Settings
+  mapping:
+    video_directory:
+      type: string
+      label: 'Video folder'
+    poster_directory:
+      type: string
+      label: 'Poster folder'
+    poster_file:
+      type: string
+      label: 'Default poster file'
+    width:
+      type: integer
+      label: 'Default player width'
+    height:
+      type: integer
+      lable: 'Default player height'
diff --git a/migrations/d7_videojs_filter_settings.yml b/migrations/d7_videojs_filter_settings.yml
new file mode 100644
index 0000000..aa2ef9d
--- /dev/null
+++ b/migrations/d7_videojs_filter_settings.yml
@@ -0,0 +1,23 @@
+id: d7_videojs_filter_settings
+label: 'Migrate VideoJS configuration'
+migration_tags:
+  - Drupal 7
+  - Configuration
+source:
+  plugin: variable
+  variables:
+    - videojs_filter_video_directory
+    - videojs_filter_poster_directory
+    - videojs_filter_poster_file
+    - videojs_filter_width
+    - videojs_filter_height
+process:
+  video_directory: videojs_filter_video_directory
+  poster_directory: videojs_filter_poster_directory
+  poster_file: videojs_filter_poster_file
+  width: videojs_filter_width
+  height: videojs_filter_height
+destination:
+  plugin: config
+  config_name: videojs_filter.settings
+  destination_module: videojs_filter
diff --git a/src/Form/VideoJsFilterSettingsForm.php b/src/Form/VideoJsFilterSettingsForm.php
new file mode 100644
index 0000000..0e002c1
--- /dev/null
+++ b/src/Form/VideoJsFilterSettingsForm.php
@@ -0,0 +1,162 @@
+<?php
+
+namespace Drupal\videojs_filter\Form;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
+use Drupal\Core\File\FileSystem;
+use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\Form\FormStateInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Defines a form to configure VideoJS Filter settings for this site.
+ */
+class VideoJsFilterSettingsForm extends ConfigFormBase implements ContainerInjectionInterface {
+
+  /**
+   * The file system service.
+   *
+   * @var \Drupal\Core\File\FileSystem
+   */
+  protected $fileSystem;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormId() {
+    return 'videojs_filter_settings_form';
+  }
+
+  /**
+   * VideoJsFilterSettingsForm constructor.
+   *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The config factory.
+   * @param \Drupal\Core\File\FileSystem $file_system
+   *   The file system service.
+   */
+  public function __construct(ConfigFactoryInterface $config_factory, FileSystem $file_system) {
+    parent::__construct($config_factory);
+
+    $this->fileSystem = $file_system;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container) {
+    return new static(
+      $container->get('config.factory'),
+      $container->get('file_system')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function buildForm(array $form, FormStateInterface $form_state) {
+    $form = parent::buildForm($form, $form_state);
+    $config = $this->config('videojs_filter.settings');
+
+    $form['video_directory'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Video folder'),
+      '#default_value' => $config->get('video_directory'),
+      '#field_prefix' => file_create_url('public://'),
+      '#required' => TRUE,
+    ];
+
+    $form['poster_directory'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Poster folder'),
+      '#default_value' => $config->get('poster_directory'),
+      '#field_prefix' => file_create_url('public://'),
+      '#required' => TRUE,
+    ];
+
+    $form['poster_file'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Default poster file'),
+      '#default_value' => $config->get('poster_file'),
+    ];
+
+    $width = $config->get('width');
+    if (!$width && $width !== 0) {
+      $width = 320;
+    }
+    $form['width'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Default player width'),
+      '#default_value' => $width,
+      '#required' => TRUE,
+    ];
+
+    $height = $config->get('height');
+    if (!$height && $height !== 0) {
+      $height = 240;
+    }
+    $form['height'] = [
+      '#type' => 'textfield',
+      '#title' => $this->t('Default player height'),
+      '#default_value' => $height,
+      '#required' => TRUE,
+    ];
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    $path_video = $form_state->getValue('video_directory');
+    if (!file_exists($this->fileSystem->realpath('public://') . '/' . $path_video)) {
+      $form_state->setErrorByName($form['video_directory'], $this->t('The video folder is not exist.'));
+    }
+
+    $path_poster = $form_state->getValue('poster_directory');
+    if (!file_exists($this->fileSystem->realpath('public://') . '/' . $path_poster)) {
+      $form_state->setErrorByName($form['poster_directory'], $this->t('The poster folder is not exist.'));
+    }
+
+    $poster_file = $form_state->getValue('poster_file');
+    if (!file_exists($this->fileSystem->realpath('public://') . '/' . $path_poster . '/' . $poster_file)) {
+      $form_state->setErrorByName($form['poster_file'], $this->t('The default poster file is not exist.'));
+    }
+
+    if (!ctype_digit($form_state->getValue('width'))) {
+      $form_state->setErrorByName($form['width'], $this->t('Default player width - incorrect value.'));
+    }
+
+    if (!ctype_digit($form_state->getValue('height'))) {
+      $form_state->setErrorByName($form['height'], $this->t('Default player height - incorrect value.'));
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+
+    $config = $this->config('videojs_filter.settings');
+    $config
+      ->set('video_directory', $form_state->getValue('video_directory'))
+      ->set('poster_directory', $form_state->getValue('poster_directory'))
+      ->set('poster_file', $form_state->getValue('poster_file'))
+      ->set('width', $form_state->getValue('width'))
+      ->set('height', $form_state->getValue('height'))
+      ->save();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getEditableConfigNames() {
+    return [
+      'videojs_filter.settings',
+    ];
+  }
+
+}
diff --git a/src/Plugin/Filter/VideoJsFilter.php b/src/Plugin/Filter/VideoJsFilter.php
new file mode 100644
index 0000000..5b52592
--- /dev/null
+++ b/src/Plugin/Filter/VideoJsFilter.php
@@ -0,0 +1,153 @@
+<?php
+
+namespace Drupal\videojs_filter\Plugin\Filter;
+
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Url;
+use Drupal\filter\FilterProcessResult;
+use Drupal\filter\Plugin\FilterBase;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+
+/**
+ * Provides a filter for VideoJS.
+ *
+ * @Filter(
+ *   id = "videojs_filter",
+ *   title = @Translation("VideoJS Filter"),
+ *   description = @Translation("Replace [videojs:{video_file}|poster:{poster_file}|width:{width}|height:{height}] to video with VideoJS player. {video_file} is required argument."),
+ *   type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
+ * )
+ */
+class VideoJsFilter extends FilterBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * The config factory.
+   *
+   * @var \Drupal\Core\Config\ConfigFactoryInterface
+   */
+  protected $configFactory;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function __construct(array $configuration, $plugin_id, $plugin_definition, ConfigFactoryInterface $config_factory, RendererInterface $renderer) {
+    parent::__construct($configuration, $plugin_id, $plugin_definition);
+
+    $this->configFactory = $config_factory;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $configuration,
+      $plugin_id,
+      $plugin_definition,
+      $container->get('config.factory'),
+      $container->get('renderer')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function defaultConfiguration() {
+    return ['controls' => TRUE] + parent::defaultConfiguration();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, FormStateInterface $form_state) {
+    $form['controls'] = [
+      '#type' => 'checkbox',
+      '#title' => $this->t('Show controls'),
+      '#default_value' => $this->settings['controls'],
+    ];
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function process($text, $langcode) {
+    $result = new FilterProcessResult($text);
+    $config = $this->configFactory->get('videojs_filter.settings');
+    preg_match_all("/(\[)videojs(\:)[^\]]*(\])/s", $text, $matches);
+    $orig_match = [];
+    $html_code = [];
+    $public_uri = 'public://';
+    $video_dir = $config->get('video_directory');
+    $poster_dir = $config->get('poster_directory');
+    $poster_file = $config->get('poster_file');
+    $player_width = $config->get('width');
+    $player_height = $config->get('height');
+    $i = 0;
+    foreach ($matches[0] as $match) {
+      $attr = [];
+      $orig_match[] = $match;
+      $match = str_replace(['[', ']', '{', '}'], '', $match);
+      $options = explode('|', $match);
+      foreach ($options as $opt) {
+        $tmp = explode(':', $opt);
+        $attr[$tmp[0]] = trim($tmp[1]);
+      }
+      if (isset($attr['videojs'])) {
+        $uri = $public_uri;
+        if ($video_dir) {
+          $uri .= $video_dir . '/';
+        }
+        $uri .= $attr['videojs'];
+
+        $file_poster['uri'] = (isset($attr['poster'])) ? $public_uri . $poster_dir . '/' . $attr['poster'] : $public_uri . $poster_dir . '/' . $poster_file;
+        $file_ext = pathinfo($file_poster['uri']);
+        $file_poster['filemime'] = 'image/' . $file_ext['extension'];
+
+        $items_video[] = Url::fromUri(file_create_url($uri));
+        // @note poster is not currently supported.
+        // @see https://www.drupal.org/project/videojs/issues/2782677
+        $attributes['poster'] = Url::fromUri(file_create_url($file_poster['uri']));
+        $attributes['width'] = (isset($attr['width'])) ? $attr['width'] : $player_width;
+        $attributes['height'] = (isset($attr['height'])) ? $attr['height'] : $player_height;
+        $attributes['controls'] = $this->settings['controls'];
+
+        $video = [
+          '#theme' => 'videojs',
+          '#items' => $items_video,
+          '#player_attributes' => $attributes,
+          '#attached' => [
+            'library' => [
+              'videojs/videojs',
+            ],
+          ],
+        ];
+        $html_code[] = '<div id="videojs-filter-wrapper-video-' . $i . '">' . $this->renderer->render($video) . '</div>';
+        $i++;
+      }
+    }
+    $text = str_replace($orig_match, $html_code, $text);
+
+    $result->setProcessedText($text);
+    return $result;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function tips($long = FALSE) {
+    return $this->t('<em>Replace [videojs:{video_file}|poster:{poster_file}|width:{width}|height:{height}]</em> to video with VideoJS player.');
+  }
+
+}
diff --git a/videojs_filter.info b/videojs_filter.info
deleted file mode 100644
index 6da8b1c..0000000
--- a/videojs_filter.info
+++ /dev/null
@@ -1,6 +0,0 @@
-name = VideoJS Filter
-description = Allow to insert videos directly into Drupal nody bodies, blocks etc. 
-dependencies[] = videojs
-configure = admin/config/media/videojs_filter
-package = Media
-core = 7.x
diff --git a/videojs_filter.info.yml b/videojs_filter.info.yml
new file mode 100644
index 0000000..1b540d8
--- /dev/null
+++ b/videojs_filter.info.yml
@@ -0,0 +1,8 @@
+name: 'VideoJS Filter'
+description: 'Allow to insert videos directly into Drupal nody bodies, blocks etc.'
+configure: videojs_filter.config
+package: Media
+core_version_requirement: ^8 || ^9
+type: module
+dependencies:
+  - drupal:videojs
diff --git a/videojs_filter.links.menu.yml b/videojs_filter.links.menu.yml
new file mode 100644
index 0000000..5aaa32e
--- /dev/null
+++ b/videojs_filter.links.menu.yml
@@ -0,0 +1,5 @@
+videojs_filter.config:
+  title: 'VideoJS Filter'
+  description: 'Configure the settings for the VideoJS Filter module.'
+  parent: system.admin_config_media
+  route_name: videojs_filter.config
diff --git a/videojs_filter.module b/videojs_filter.module
deleted file mode 100644
index 17f2d33..0000000
--- a/videojs_filter.module
+++ /dev/null
@@ -1,212 +0,0 @@
-<?php
-/**
-* @file
-* 
-*/
-
-/*
- * Implements hook_menu().
- */
-function videojs_filter_menu() {
-  
-  $items = array();
-
-  $items['admin/config/media/videojs_filter'] = array(
-    'title' => 'VideoJS Filter',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('videojs_filter_settings_form'),
-    'access arguments' => array('administer site configuration'),
-    'description' => 'Configure the settings for the VideoJS Filter module.',
-  );
-  
-  return $items;
-}
-
-/**
-*  create settings form
-* 
-*/
-function videojs_filter_settings_form() {
-  
-  $form = array();
-
-  $form['videojs_filter_video_directory'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Video folder'),
-    '#default_value' => variable_get('videojs_filter_video_directory', ''),
-    '#field_prefix' => file_create_url('public://'),
-    '#required' => TRUE,
-  );
-
-  $form['videojs_filter_poster_directory'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Poster folder'),
-    '#default_value' => variable_get('videojs_filter_poster_directory', ''),
-    '#field_prefix' => file_create_url('public://'),
-    '#required' => TRUE,
-  );
-  
-  $form['videojs_filter_poster_file'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Default poster file'),
-    '#default_value' => variable_get('videojs_filter_poster_file', ''),
-    '#required' => TRUE,
-  );
-
-  $form['videojs_filter_width'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Default player width'),
-    '#default_value' => variable_get('videojs_filter_width', 320),
-    '#required' => TRUE,
-  );
-
-  $form['videojs_filter_height'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Default player height'),
-    '#default_value' => variable_get('videojs_filter_height', 240),
-    '#required' => TRUE,
-  );
-
-  $form = system_settings_form($form);
-  $form['#validate'][] = 'videojs_filter_settings_form_validate';
-  $form['#submit'][] = 'videojs_filter_settings_form_submit';
-
-  return $form;
-}
-
-/**
- * Validation function to validate the videojs_filter_settings_form() form.
- */
-function videojs_filter_settings_form_validate($form, &$form_state) {
-    
-    $path_video = $form_state['values']['videojs_filter_video_directory'];
-    if (!file_exists( drupal_realpath('public://') . '/' . $path_video)) {
-        form_error($form['videojs_filter_video_directory'], t('The video folder is not exist.'));
-    }
-
-    $path_poster = $form_state['values']['videojs_filter_poster_directory'];
-    if (!file_exists( drupal_realpath('public://') . '/' . $path_poster)) {
-        form_error($form['videojs_filter_poster_directory'], t('The poster folder is not exist.'));
-    }
-
-    $poster_file = $form_state['values']['videojs_filter_poster_file'];
-    if (!file_exists( drupal_realpath('public://') . '/' . $path_poster . '/' . $poster_file)) {
-        form_error($form['videojs_filter_poster_file'], t('The default poster file is not exist.'));
-    }
-
-    if (!ctype_digit($form_state['values']['videojs_filter_width'])) {
-        form_error($form['videojs_filter_width'], t('Default player width - incorrect value.'));
-    }
-
-    if (!ctype_digit($form_state['values']['videojs_filter_height'])) {
-        form_error($form['videojs_filter_height'], t('Default player height - incorrect value.'));
-    }
-}
-
-/**
- * Submit handler for the videojs_filter_settings_form() form.
- */
-function videojs_filter_settings_form_submit($form, &$form_state) {
-
-    variable_set('videojs_filter_video_directory', $form_state['values']['videojs_filter_video_directory']);
-    variable_set('videojs_filter_poster_directory', $form_state['values']['videojs_filter_poster_directory']);
-    variable_set('videojs_filter_poster_file', $form_state['values']['videojs_filter_poster_file']);
-    variable_set('videojs_filter_width', $form_state['values']['videojs_filter_width']);
-    variable_set('videojs_filter_height', $form_state['values']['videojs_filter_height']);
-}
-
-/*
- * Implements hook_filter_info().
- */
-function videojs_filter_filter_info() {
-  
-  $filters['videojs_filter'] = array(
-    'title' => t('VideoJS Filter'),
-    'description' => t('Replace [videojs:{video_file}|poster:{poster_file}|width:{width}|height:{height}] to video with VideoJS player. {video_file} is required argument.'),
-    'prepare callback' => '_videojs_filter_filter_prepare',
-    'process callback' => '_videojs_filter_filter_process',
-    'tips callback' => '_videojs_filter_filter_tips',
-  );
-
-  return $filters;
-}
-
-/**
-* filter prepare callback.
-*/
-function _videojs_filter_filter_prepare($text, $filter) {
-  
-  return $text;
-}
-
-/**
- * Time filter process callback.
- */
-function _videojs_filter_filter_process($text, $filter) {
-  
-  preg_match_all("/(\[)videojs(\:)[^\]]*(\])/s", $text, $matches);
-  $orig_match = array();
-  $html_code = array();
-  $public_uri = 'public://';
-  $video_dir = variable_get('videojs_filter_video_directory');
-  $poster_dir = variable_get('videojs_filter_poster_directory');
-  $poster_file = variable_get('videojs_filter_poster_file');
-  $player_width = variable_get('videojs_filter_width', 320);
-  $player_height = variable_get('videojs_filter_height', 240);
-  $i = 0;
-  foreach ($matches[0] as $match) {
-    $attr = array();
-    $file = array();
-    $orig_match[] = $match;
-    $match = str_replace( array('[', ']', '{', '}'), '', $match );
-    $options = explode('|', $match);
-    foreach ($options as $index => $opt) {
-        $tmp = explode(':', $opt);
-        $attr[$tmp[0]] = trim($tmp[1]);
-    }
-    if (isset($attr['videojs'])) {
-        $file_ext = pathinfo($attr['videojs']);
-        $uri = $public_uri . $video_dir . '/' . $attr['videojs'];
-        
-        $file['uid'] = 1;
-        $file['display'] = 1;
-        $file['filename'] = $attr['videojs'];
-        $file['filemime'] = 'video/' . $file_ext['extension'];
-        $file['filesize'] = filesize($uri);
-        $file['uri'] = $uri;
-        $file['status'] = 1;
-
-        $file_poster['uri'] = (isset($attr['poster'])) ? $public_uri . $poster_dir . '/' . $attr['poster'] : $public_uri . $poster_dir . '/' . $poster_file;
-        $file_ext = pathinfo($file_poster['uri']);
-        $file_poster['filemime'] = 'image/' . $file_ext['extension'];
-
-        $items_video['mp4'] = $file;
-        $items_video['poster'] = $file_poster;
-
-        $attributes['width']  = (isset($attr['width'] )) ?  $attr['width'] : $player_width;
-        $attributes['height'] = (isset($attr['height'])) ?  $attr['height'] : $player_height;
-
-        $html_code[] = '<div id="videojs-filter-wrapper-video-' . $i . '">'.theme('videojs', array('items' => $items_video, 'player_id' => 'videojs-header-' . $i, 'attributes' => $attributes)) . '</div>';
-        $i++;
-     } 
-  } 
-  $text = str_replace($orig_match, $html_code, $text);
-
-  return $text;
-}
-
-/**
-*  Tips
-*/
-function _videojs_filter_filter_tips($filter, $format, $long = FALSE) {
-
-  return t('<em>Replace [videojs:{video_file}|poster:{poster_file}|width:{width}|height:{height}]</em> to video with VideoJS player.');
-} 
-
-/**
-*  Implement Hook_init
-*/
-function videojs_filter_init() {
-
-  videojs_add();
-}
\ No newline at end of file
diff --git a/videojs_filter.routing.yml b/videojs_filter.routing.yml
new file mode 100644
index 0000000..6d1b6c2
--- /dev/null
+++ b/videojs_filter.routing.yml
@@ -0,0 +1,7 @@
+videojs_filter.config:
+  path: '/admin/config/media/videojs_filter'
+  defaults:
+    _title: 'VideoJS Filter'
+    _form: '\Drupal\videojs_filter\Form\VideoJsFilterSettingsForm'
+  requirements:
+    _permission: 'administer site configuration'
