Fetching latest headlines…
I Built a Safari Extension That Shows When Your YouTube Video Ends
NORTH AMERICA
πŸ‡ΊπŸ‡Έ United Statesβ€’May 21, 2026

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

3 views0 likes0 comments
Originally published byDev.to

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.

The trickier part was getting it to feel native.

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 <span> 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. Would love to know if anyone finds this actually useful day-to-day.

Comments (0)

Sign in to join the discussion

Be the first to comment!