I Built a Safari Extension That Shows When Your YouTube Video Ends

Published: (May 21, 2026 at 07:28 PM EDT)
3 min read
Source: Dev.to

Source: Dev.to

Cover image for I Built a Safari Extension That Shows When Your YouTube Video Ends

Ever put on a YouTube video and wondered if you’ll finish it before you have to leave, go to sleep, or get back to work? I had that thought one too many times, so I built a small Safari extension to solve it.

It adds the end time directly inside YouTube’s native time bubble:

0:20 / 7:23 · ends 11:44pm

That’s it. Simple, but surprisingly useful.

How it works

The core logic is about three lines:

const remainingSec = (video.duration - video.currentTime) / video.playbackRate;
const endDate = new Date(Date.now() + remainingSec * 1000);

Grab the remaining seconds, divide by the playback rate (so it works correctly at 0.5×, 1.5×, 2× etc.), add it to the current time. Done.

Injecting into YouTube’s player

YouTube’s player controls are built with a bunch of class‑named spans. The time bubble you see is a .ytp-time-display element containing:

  • .ytp-time-current — the current position
  • .ytp-time-separator — the /
  • .ytp-time-duration — the total length

I inject a new “ directly after .ytp-time-duration, so the end time sits inside the same pill — inheriting YouTube’s exact font, colour and sizing automatically without needing to hardcode any styles.

const duration = document.querySelector('.ytp-time-duration');
duration.insertAdjacentElement('afterend', mySpan);

The YouTube SPA problem

YouTube is a single‑page app, so navigating between videos doesn’t trigger a full page reload. The player DOM gets rebuilt, which means my injected element disappears.

The fix is two‑pronged:

1. Listen for YouTube’s own navigation events

['yt-navigate-finish', 'yt-page-data-updated', 'yt-player-updated'].forEach(evt => {
  document.addEventListener(evt, reinject);
});

2. A MutationObserver as a fallback

const obs = new MutationObserver(() => {
  if (!document.getElementById('yt-end-time-ext') &&
      document.querySelector('.ytp-time-duration')) {
    reinject();
  }
});
obs.observe(document.body, { childList: true, subtree: true });

Between these two, re‑injection is reliable across every navigation scenario I’ve tested.

Packaging for Safari

This is where it gets slightly annoying. Safari doesn’t load unpacked extensions the way Chrome does — you need to wrap it in a macOS app using Xcode.

Apple provides a converter tool that does the heavy lifting:

xcrun safari-web-extension-converter ./youtube-end-time-extension

This generates a full Xcode project with your extension embedded. You hit ⌘R, it builds a small launcher app, and then you enable the extension in Safari’s settings. For open‑source projects this works fine — anyone can clone the repo and build it themselves in a couple of minutes.

The one gotcha: Safari requires you to re‑enable Develop → Allow Unsigned Extensions every time you restart the browser. A minor annoyance, but not a dealbreaker for a personal tool.

What I’d add next

  • A subtle tooltip on hover showing the exact end time with seconds
  • Auto‑hiding when a video is paused for a long time (since the end time becomes meaningless)
  • Firefox/Chrome support via the same Manifest V3 codebase — it’s already compatible, just needs packaging

Try it

The full source is on GitHub — it’s about 100 lines of vanilla JS and works on any Mac with Xcode installed.

github.com/yourusername/youtube-end-time

If you build something on top of it or spot a bug, PRs are open. I’d love to know if anyone finds this actually useful day‑to‑day.

0 views
Back to Blog

Related posts

Read more »