<head>
 <meta charset="UTF-8">
 <script>
+const MS_IN_S = 1000;
 const RETRY_INTERVAL_S = 5;
 const PATH_EVENTS = "/{{page_names.events}}";
 const PATH_PLAYER = "/{{page_names.player}}";
         events_stream.close();
         if (while_connecting) {
             console.log("Error seemed connection-related, trying reconnect.");
-            setTimeout(connect_events, RETRY_INTERVAL_S * 1000);
+            setTimeout(connect_events, RETRY_INTERVAL_S * MS_IN_S);
         } else {
             console.log("Error does not seem connection-related, therefore aborting.");
         }
     });
 }
 
+var timestamp_interval = null;
+
 event_handlers.player = function(data) {
     const div = document.getElementById("player_controls");
     div.innerHTML = "";
     add_player_btn_to(div, "next", "next", !data.can_play);
     add_player_btn_to(div, data.is_playing ? "pause" : "play", "play", !data.can_play);
     add_text_to(div, " · ");
-    add_text_to(div, data.is_running ? (data.is_playing ? "playing:" : "paused:")
-                                     : "stopped" + (data.title ? ':' : ''));
+    add_text_to(div, data.is_running ? (data.is_playing ? "playing" : "paused")
+                                     : "stopped");
     if (data.title_digest) {
+        function format_seconds(total_seconds) {
+            if (total_seconds < 0) {
+               return "?";
+            }
+            const seconds = total_seconds % 60;
+            const minutes = Math.floor(total_seconds / 60);
+            return `${minutes}:` + (seconds < 10 ? '0' : '') + `${seconds}`
+        }
+        add_text_to(div, " (");
+        const timestamp_span = add_child_to("span", div);
+        clearInterval(timestamp_interval);
+        let timestamp = data.timestamp;
+        timestamp_span.textContent = format_seconds(timestamp);
+        if (data.is_playing) {
+            timestamp_interval = setInterval(function() {
+                 timestamp += 1;
+                 timestamp_span.textContent = format_seconds(timestamp);
+            }, MS_IN_S / data.speed);
+        }
+        add_text_to(div, "/" + format_seconds(data.duration) + "): ");
         add_a_to(div, data.title, `${PATH_PREFIX_FILE}${data.title_digest}`);
         add_text_to(div, " · ");
         add_tag_links_to(div, data.title_tags);
+
     }
 };
 
 
             self.server.player.move_entry(int(command.split('_')[1]), False)
         elif command.startswith('rm_'):
             self.server.player.remove_by_idx(int(command.split('_')[1]))
-        elif command.startswith('inject_') or command.startswith('injectplay_'):
+        elif (command.startswith('inject_')
+              or command.startswith('injectplay_')):
             command, digest = command.split('_', 1)
             with DbConn() as conn:
                 file = VideoFile.get_one(conn, Hash.from_b64(digest))
                             'is_running': self.server.player.is_running,
                             'is_playing': self.server.player.is_playing,
                             'can_play': self.server.player.can_play,
+                            'timestamp': self.server.player.timestamp,
+                            'duration': self.server.player.duration,
+                            'speed': self.server.player.speed,
                             'title_tags': tags,
                             'title_digest': digest,
                             'title': title
 
         self._monitoring_kill: bool = False
         self._kill_queue: Queue = Queue()
         self.playlist: list[VideoFile] = []
+        self.speed = -1.0
+        self.timestamp = -1
+        self.duration = -1
         self.load_files_and_mpv()
 
     def _signal_update(self) -> None:
-        """Update .last_update as signal player state has changed relevantly"""
+        """Update .last_update as signal player state has changed relevantly.
+
+        If possible, also updates current player timestamp.
+        """
         self.last_update = _now_string()
+        if self._mpv:
+            self.timestamp = (int(self._mpv.time_pos) if self._mpv.time_pos
+                              else -1)
 
     def _monitor_kill(self) -> None:
         """Properly enforce mpv shutdown from direct interaction with mpv
         - bind starting of files to ._signal_update and setting ._idx to MPV's
           own playlist position index
         - bind ending last file to re-starting at playlist start
+        - bind what affects currently played duration, timestamp to respective
+          updates
         - start playing
         """
         self._mpv = MPV(input_default_bindings=True,
         def on_shutdown(_) -> None:
             self._kill_queue.put(True)
 
+        @self._mpv.event_callback('seek')
+        def on_seek(*_) -> None:
+            self._signal_update()
+
+        def on_duration_change(_, duration_in_s):
+            self.duration = int(duration_in_s) if duration_in_s else -1
+            self._signal_update()
+
+        def on_speed_change(_, speed):
+            self.speed = speed
+            self._signal_update()
+
         self._mpv.observe_property('pause', lambda a, b: self._signal_update())
+        self._mpv.observe_property('duration', on_duration_change)
+        self._mpv.observe_property('speed', on_speed_change)
         for path in [f.full_path for f in self.playlist]:
             self._mpv.command('loadfile', path, 'append')
         self._idx = 0
             video_id = self._to_download.pop(0)
             url = f'{YOUTUBE_URL_PREFIX}{video_id}'
             with YoutubeDL(YT_DL_PARAMS | {'progress_hooks': [hook]}) as ydl:
-                self._update_status(video_id, f'preparing download')
+                self._update_status(video_id, 'preparing download')
                 info = ydl.sanitize_info(ydl.extract_info(url, download=False))
                 for requested in info['requested_formats']:
                     estimated_total += requested['filesize_approx']