diff --git a/app/src/main/java/org/schabi/newpipe/player/Player.java b/app/src/main/java/org/schabi/newpipe/player/Player.java
index b09593c1739..dd2cef04054 100644
--- a/app/src/main/java/org/schabi/newpipe/player/Player.java
+++ b/app/src/main/java/org/schabi/newpipe/player/Player.java
@@ -122,6 +122,7 @@
 import org.schabi.newpipe.util.StreamTypeUtil;
 import org.schabi.newpipe.util.image.CoilHelper;
 
+import java.time.temporal.ChronoUnit;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.IntStream;
@@ -263,6 +264,9 @@ public final class Player implements PlaybackListener, Listener {
     @NonNull
     private final HistoryRecordManager recordManager;
 
+    java.util.Timer sleepTimer;
+    java.time.Instant sleepTimerEnd;
+
 
     /*//////////////////////////////////////////////////////////////////////////
     // Constructor
@@ -2262,6 +2266,44 @@ public void setAudioTrack(@Nullable final String audioTrackId) {
     }
 
 
+    public void setSleepTimer(@Nullable final long sleepMinutes) {
+        if (sleepTimer != null) {
+            sleepTimer.cancel();
+            sleepTimer.purge();
+            sleepTimer = null;
+        }
+
+        sleepTimerEnd = java.time.Instant.now().plus(sleepMinutes, ChronoUnit.MINUTES);
+
+        sleepTimer = new java.util.Timer();
+        //final Player thisPlayer = this;
+        final java.util.TimerTask task = new java.util.TimerTask() {
+
+
+            @Override
+            public void run() {
+                if (java.time.Instant.now().compareTo(sleepTimerEnd) >= 0) {
+                    UIs.call(playerUi -> playerUi.onSleepTimerUpdate(0));
+                    cancelSleepTimer();
+                    return;
+                }
+
+                final long remainingMinutes = java.time.Instant.now().until(sleepTimerEnd,
+                        ChronoUnit.MINUTES);
+                UIs.call(playerUi -> playerUi.onSleepTimerUpdate(remainingMinutes + 1));
+            }
+        };
+        sleepTimer.schedule(task, 1000, 1000);
+    }
+
+    public void cancelSleepTimer() {
+        if (sleepTimer != null) {
+            sleepTimer.cancel();
+            sleepTimer.purge();
+            sleepTimer = null;
+        }
+    }
+
     @NonNull
     public Context getContext() {
         return context;
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
index 57e2ec2a2cf..418436b2d26 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/PlayerUi.java
@@ -209,4 +209,11 @@ public void onPlayQueueEdited() {
      */
     public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
     }
+
+    /**
+     * @param remainingTime the remaining sleep timer time, set to 0 to pause the player and
+     *                      disable the sleep timer
+     */
+    public void onSleepTimerUpdate(final long remainingTime) {
+    }
 }
diff --git a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
index e96873de52c..91d9400932c 100644
--- a/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
+++ b/app/src/main/java/org/schabi/newpipe/player/ui/VideoPlayerUi.java
@@ -128,12 +128,14 @@ private enum PlayButtonAction {
     private static final int POPUP_MENU_ID_AUDIO_TRACK = 70;
     private static final int POPUP_MENU_ID_PLAYBACK_SPEED = 79;
     private static final int POPUP_MENU_ID_CAPTION = 89;
+    private static final int POPUP_MENU_ID_SLEEP_TIMER = 90; // TODO is 90 still available?
 
     protected boolean isSomePopupMenuVisible = false;
     private PopupMenu qualityPopupMenu;
     private PopupMenu audioTrackPopupMenu;
     protected PopupMenu playbackSpeedPopupMenu;
     private PopupMenu captionPopupMenu;
+    private PopupMenu sleepTimerPopupMenu;
 
 
     /*//////////////////////////////////////////////////////////////////////////
@@ -186,6 +188,8 @@ private void initViews() {
         audioTrackPopupMenu = new PopupMenu(themeWrapper, binding.audioTrackTextView);
         playbackSpeedPopupMenu = new PopupMenu(context, binding.playbackSpeed);
         captionPopupMenu = new PopupMenu(themeWrapper, binding.captionTextView);
+        sleepTimerPopupMenu = new PopupMenu(themeWrapper, binding.sleepTimer);
+        buildSleepTimerMenu();
 
         binding.progressBarLoadingPanel.getIndeterminateDrawable()
                 .setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.MULTIPLY));
@@ -204,6 +208,9 @@ protected void initListeners() {
         binding.audioTrackTextView.setOnClickListener(
                 makeOnClickListener(this::onAudioTracksClicked));
         binding.playbackSpeed.setOnClickListener(makeOnClickListener(this::onPlaybackSpeedClicked));
+        binding.sleepTimer.setOnClickListener(makeOnClickListener(this::onSleepTimerClicked));
+        binding.sleepTimerCancel.setOnClickListener(
+                makeOnClickListener(this::onSleepTimerCancelClicked));
 
         binding.playbackSeekBar.setOnSeekBarChangeListener(this);
         binding.captionTextView.setOnClickListener(makeOnClickListener(this::onCaptionClicked));
@@ -1239,6 +1246,49 @@ private void buildCaptionMenu(@NonNull final List availableLanguages) {
         }
     }
 
+    private void buildSleepTimerMenu() {
+        if (sleepTimerPopupMenu == null) {
+            return;
+        }
+        qualityPopupMenu.getMenu().removeGroup(POPUP_MENU_ID_SLEEP_TIMER);
+
+        final Resources res = context.getResources();
+        sleepTimerPopupMenu.getMenu().add(POPUP_MENU_ID_SLEEP_TIMER, 0, 0,
+                res.getString(R.string.sleep_timer_popup_title));
+
+        final String[] descriptions = context.getResources().getStringArray(
+                R.array.sleep_timer_description);
+        final int[] values = context.getResources().getIntArray(
+                R.array.sleep_timer_value);
+        for (int i = 0; i < descriptions.length && i < values.length; i++) {
+            String description = "";
+            try {
+                final int hours = values[i] / 60;
+                final int minutes = values[i] % 60;
+                if (hours != 0) {
+                    description += String.format(res.getQuantityString(R.plurals.hours, hours),
+                            hours);
+                }
+
+                if (minutes != 0) {
+                    if (hours != 0) {
+                        description += " ";
+                    }
+                    description += String.format(res.getQuantityString(R.plurals.minutes, minutes),
+                            minutes);
+                }
+            } catch (final Resources.NotFoundException ignored) {
+                // if this happens, the translation is missing,
+                // and the english string will be displayed instead
+                description = descriptions[i];
+            }
+            sleepTimerPopupMenu.getMenu().add(POPUP_MENU_ID_SLEEP_TIMER, i + 1, i + 1, description);
+        }
+
+        sleepTimerPopupMenu.setOnMenuItemClickListener(this);
+        sleepTimerPopupMenu.setOnDismissListener(this);
+    }
+
     protected abstract void onPlaybackSpeedClicked();
 
     private void onQualityClicked() {
@@ -1255,6 +1305,19 @@ private void onAudioTracksClicked() {
         isSomePopupMenuVisible = true;
     }
 
+    private void onSleepTimerClicked() {
+        sleepTimerPopupMenu.show();
+        isSomePopupMenuVisible = true;
+    }
+
+    private void onSleepTimerCancelClicked() {
+        player.cancelSleepTimer();
+
+        binding.sleepTimerCancel.setVisibility(View.INVISIBLE);
+        binding.sleepTimerTextView.setVisibility(View.INVISIBLE);
+        binding.sleepTimerTextView.setText("0:00");
+    }
+
     /**
      * Called when an item of the quality selector or the playback speed selector is selected.
      */
@@ -1278,8 +1341,12 @@ public boolean onMenuItemClick(@NonNull final MenuItem menuItem) {
 
             player.setPlaybackSpeed(speed);
             binding.playbackSpeed.setText(formatSpeed(speed));
+        } else if (menuItem.getGroupId() == POPUP_MENU_ID_SLEEP_TIMER) {
+            onSleepTimerItemClick(menuItem);
+            return true;
         }
 
+
         return false;
     }
 
@@ -1324,6 +1391,24 @@ private void onAudioTrackItemClick(@NonNull final MenuItem menuItem) {
         binding.audioTrackTextView.setText(menuItem.getTitle());
     }
 
+    private void onSleepTimerItemClick(@NonNull final MenuItem menuItem) {
+        final int menuItemIndex = menuItem.getItemId();
+        if (menuItemIndex == 0) {
+            return;
+        }
+
+        final int index = menuItemIndex - 1;
+        final int sleepTime = context.getResources().getIntArray(R.array.sleep_timer_value)[index];
+        final long remainingTimeHours = sleepTime / 60;
+        final long remainingTimeMinutes = sleepTime % 60;
+        final String text = String.format("%d:%02d", remainingTimeHours, remainingTimeMinutes);
+
+        player.setSleepTimer(sleepTime);
+        binding.sleepTimerCancel.setVisibility(View.VISIBLE);
+        binding.sleepTimerTextView.setVisibility(View.VISIBLE);
+        binding.sleepTimerTextView.setText(text);
+    }
+
     /**
      * Called when some popup menu is dismissed.
      */
@@ -1561,6 +1646,32 @@ public void onVideoSizeChanged(@NonNull final VideoSize videoSize) {
     }
     //endregion
 
+    @Override
+    public void onSleepTimerUpdate(final long remainingTime) {
+        if (remainingTime == 0) {
+            binding.sleepTimerTextView.post(new Runnable() {
+                public void run() {
+                    player.pause();
+                    binding.sleepTimerCancel.setVisibility(View.INVISIBLE);
+                    binding.sleepTimerTextView.setVisibility(View.INVISIBLE);
+                    binding.sleepTimerTextView.setText("0:00");
+                }
+            });
+            return;
+        }
+
+        final long remainingTimeHours = remainingTime / 60;
+        final long remainingTimeMinutes = remainingTime % 60;
+        final String text = String.format("%d:%02d", remainingTimeHours, remainingTimeMinutes);
+
+        // Since this callback can/will be called from a different thread, we need to run set
+        // the code in the UI thread
+        binding.sleepTimerTextView.post(new Runnable() {
+            public void run() {
+                binding.sleepTimerTextView.setText(text);
+            }
+        });
+    }
 
     /*//////////////////////////////////////////////////////////////////////////
     // SurfaceHolderCallback helpers
diff --git a/app/src/main/res/drawable/ic_alarm.xml b/app/src/main/res/drawable/ic_alarm.xml
new file mode 100644
index 00000000000..dfc893e1e96
--- /dev/null
+++ b/app/src/main/res/drawable/ic_alarm.xml
@@ -0,0 +1,10 @@
+
+    
+
diff --git a/app/src/main/res/drawable/ic_alarm_off.xml b/app/src/main/res/drawable/ic_alarm_off.xml
new file mode 100644
index 00000000000..156a4320ccc
--- /dev/null
+++ b/app/src/main/res/drawable/ic_alarm_off.xml
@@ -0,0 +1,10 @@
+
+    
+
diff --git a/app/src/main/res/layout/player.xml b/app/src/main/res/layout/player.xml
index 99b514bb090..289cfa21c38 100644
--- a/app/src/main/res/layout/player.xml
+++ b/app/src/main/res/layout/player.xml
@@ -262,124 +262,188 @@
                     android:layout_height="wrap_content"
                     android:gravity="top"
                     android:visibility="invisible"
-                    tools:ignore="RtlHardcoded"
-                    tools:visibility="visible">
+                    android:orientation="vertical">
 
-                    
-
-                    
+                        android:gravity="top"
+                        tools:ignore="RtlHardcoded">
 
                         
+                            tools:ignore="HardcodedText,RtlHardcoded"
+                            tools:text="FIT" />
 
-                    
+                        
+
+                            
+
+                        
+
+                        
 
-                    
+                        
 
-                    
+                        
 
-                    
+                        
+
+                        
+                    
 
-                    
+                    
 
-                    
+                        
 
-                
+                        
 
+                        
+
+                        
+                    
+                
             
 
             Gefällt mir
     SoundCloud-Top-50-Seite entfernt
     SoundCloud hat die ursprünglichen Top-50-Charts abgeschafft. Der entsprechende Tab wurde von deiner Hauptseite entfernt.
+    Sleep Timer
+    Sleep Timer stellen
+    Sleep Timer beenden
 
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index d95d1270cc9..fec2403a6dc 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -50,6 +50,27 @@
         - 30000+    
+        
+
- 5 minutes+
- 10 minutes+
- 20 minutes+
- 30 minutes+
- 45 minutes+
- 1 hour+
- 2 hours+    
+    
+
- 5+
- 10+
- 20+
- 30+
- 45+
- 60+
- 120+    
+
     progressive_load_interval
     64
     exoplayer_default
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index f28a9958da0..281aba8f8f8 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -889,4 +889,7 @@
     Trending podcasts
     Trending movies and shows
     Trending music
+    Sleep timer
+    Set sleep timer
+    Cancel sleep timer