Go actual full screen on Simplepractice.com video calls

When I’m on calls with my therapist it has bugged me that I can’t actually go fullscreen with the video so I gave CGPT the code for the page and wrote up a script that expands the fullscreen video to actually be fullscreen.

Use either the console script or Tampermonkey script and try it out for yourself!

Console script

(() => {
  const TOGGLE = 'tm-full-bleed-active';
  const ROOT_SEL = 'div.bbwWj.video';
  const PANEL_SEL = `${ROOT_SEL} > div.mljTO`;
  const STREAM_CONTAINER_SEL = `${ROOT_SEL} .stream-container`;
  const PARTICIPANT_SEL = `${ROOT_SEL} .participant-container`;
  const VIDEO_SEL = `${ROOT_SEL} video`;

  // --- style injection ---
  const css = `
    html.${TOGGLE}, body.${TOGGLE}{margin:0!important;padding:0!important;overflow:hidden!important}
    html.${TOGGLE} ${PANEL_SEL}{
      position:fixed!important;inset:0!important;width:100vw!important;height:100vh!important;
      margin:0!important;padding:0!important;border:0!important;background:#000!important;
      z-index:2147483647!important;box-shadow:none!important
    }
    html.${TOGGLE} ${STREAM_CONTAINER_SEL}, html.${TOGGLE} ${PARTICIPANT_SEL}{
      position:absolute!important;inset:0!important;width:100%!important;height:100%!important;
      margin:0!important;padding:0!important;border:0!important;max-width:none!important;max-height:none!important;overflow:hidden!important;background:transparent!important
    }
    html.${TOGGLE} ${VIDEO_SEL}{
      position:absolute!important;inset:0!important;width:100%!important;height:100%!important;
      object-fit:cover!important;object-position:center center!important;display:block!important;background:#000!important;border:0!important;box-shadow:none!important;transform:none!important
    }
    html.${TOGGLE} ${ROOT_SEL} .name, html.${TOGGLE} ${ROOT_SEL} .pin-button, html.${TOGGLE} ${ROOT_SEL} .more-button{
      display:none!important;visibility:hidden!important;pointer-events:none!important
    }
    .tm-fb-btn{
      position:fixed!important;right:14px;bottom:14px;z-index:2147483647!important;
      font:600 12px/1 system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
      padding:10px 12px;border-radius:999px;background:rgba(0,0,0,.6);color:#fff;
      border:1px solid rgba(255,255,255,.2);cursor:pointer;user-select:none;backdrop-filter:blur(4px)
    }
    .tm-fb-btn:hover{background:rgba(0,0,0,.75)}
  `;
  let style = document.getElementById('tm-fb-style');
  if (!style) {
    style = document.createElement('style');
    style.id = 'tm-fb-style';
    style.textContent = css;
    document.head.appendChild(style);
  }

  // --- helpers ---
  const $ = (s, r = document) => r.querySelector(s);
  const nap = (ms) => new Promise(r => setTimeout(r, ms));
  const waitFor = async (sel, tries = 200, step = 100) => {
    while (tries-- > 0) { const el = $(sel); if (el) return el; await nap(step); }
    return null;
  };

  // --- state ---
  let btn;

  const isActive = () =>
    document.documentElement.classList.contains(TOGGLE) ||
    document.body.classList.contains(TOGGLE);

  const setActive = (on) => {
    [document.documentElement, document.body].forEach(el => el && el.classList.toggle(TOGGLE, on));
    if (btn) btn.textContent = on ? 'Exit Full-Bleed' : 'Full-Bleed';
  };

  const toggle = () => setActive(!isActive());

  // --- UI button ---
  const mountButton = () => {
    if (btn && document.body.contains(btn)) return btn;
    btn = document.createElement('button');
    btn.className = 'tm-fb-btn';
    btn.type = 'button';
    btn.textContent = isActive() ? 'Exit Full-Bleed' : 'Full-Bleed';
    btn.addEventListener('click', toggle);
    document.body.appendChild(btn);
    return btn;
  };

  // --- key bindings ---
  const bindKeys = () => {
    window.addEventListener('keydown', (e) => {
      if (e.key === 'f' || e.key === 'F') { e.preventDefault(); toggle(); }
      else if (e.key === 'Escape' && isActive()) { e.preventDefault(); setActive(false); }
    }, { capture: true });
  };

  // --- dblclick on video ---
  const bindVideoDblClick = (v) => {
    if (!v || v.dataset.tmFbBound) return;
    v.addEventListener('dblclick', (e) => { e.preventDefault(); toggle(); }, { capture: true });
    v.dataset.tmFbBound = '1';
  };

  // --- observe dynamic DOM ---
  const observe = () => {
    const mo = new MutationObserver(() => {
      const v = $(VIDEO_SEL);
      if (v) bindVideoDblClick(v);
      if (!btn && document.body) mountButton();
    });
    mo.observe(document.documentElement, { childList: true, subtree: true });
  };

  // --- bootstrap ---
  (async () => {
    await waitFor('body');
    bindKeys();
    observe();
    const v = await waitFor(VIDEO_SEL, 120, 150);
    if (v) bindVideoDblClick(v);
    mountButton();

    // Expose controls for manual use
    window.tmFullBleed = { toggle, on: () => setActive(true), off: () => setActive(false) };
    console.log('[tmFullBleed] Ready. Use tmFullBleed.toggle(), press F, double-click video, or use the button.');
  })();
})();

Tampermonkey script

// ==UserScript==
// @name         Full-Bleed Video (SimplePractice only)
// @namespace    nic.tools.video
// @version      1.0.1
// @description  Force main video to go true full-bleed (edge-to-edge) on SimplePractice video
// @author       you
// @match        https://video.simplepractice.com/*
// @run-at       document-idle
// @grant        GM_addStyle
// ==/UserScript==

(function () {
  'use strict';

  // ---------- small utils ----------
  const nap = (ms) => new Promise(r => setTimeout(r, ms));
  const q = (sel, root = document) => root.querySelector(sel);

  // Wait for an element or return null after tries*interval
  const waitFor = async (fnOrSel, tries = 200, interval = 100) => {
    while (tries-- > 0) {
      let el = null;
      try { el = typeof fnOrSel === 'function' ? fnOrSel() : q(fnOrSel); } catch {}
      if (el) return el;
      await nap(interval);
    }
    return null;
  };

  // ---------- selectors from provided markup ----------
  const ROOT_SEL = 'div.bbwWj.video';
  const PANEL_SEL = `${ROOT_SEL} > div.mljTO`;
  const STREAM_CONTAINER_SEL = `${ROOT_SEL} .stream-container`;
  const PARTICIPANT_SEL = `${ROOT_SEL} .participant-container`;
  const VIDEO_SEL = `${ROOT_SEL} video`;

  const TOGGLE_CLASS = 'tm-full-bleed-active';

  // ---------- inject styles ----------
  GM_addStyle(`
    /* Full-bleed mode root locks */
    html.${TOGGLE_CLASS}, body.${TOGGLE_CLASS} {
      margin: 0 !important;
      padding: 0 !important;
      overflow: hidden !important;
    }

    /* Pin the main panel to the viewport */
    html.${TOGGLE_CLASS} ${PANEL_SEL} {
      position: fixed !important;
      inset: 0 !important;
      width: 100vw !important;
      height: 100vh !important;
      margin: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      background: #000 !important;
      z-index: 2147483647 !important;
      box-shadow: none !important;
    }

    /* Ensure no intermediate wrapper constrains size */
    html.${TOGGLE_CLASS} ${STREAM_CONTAINER_SEL},
    html.${TOGGLE_CLASS} ${PARTICIPANT_SEL} {
      position: absolute !important;
      inset: 0 !important;
      width: 100% !important;
      height: 100% !important;
      margin: 0 !important;
      padding: 0 !important;
      border: 0 !important;
      max-width: none !important;
      max-height: none !important;
      overflow: hidden !important;
      background: transparent !important;
    }

    /* Make the video fill and crop (letterboxing avoidance) */
    html.${TOGGLE_CLASS} ${VIDEO_SEL} {
      position: absolute !important;
      inset: 0 !important;
      width: 100% !important;
      height: 100% !important;
      object-fit: cover !important;
      object-position: center center !important;
      display: block !important;
      background: #000 !important;
      border: 0 !important;
      box-shadow: none !important;
      transform: none !important;
    }

    /* Hide overlays that might intrude (adjust as needed) */
    html.${TOGGLE_CLASS} ${ROOT_SEL} .name,
    html.${TOGGLE_CLASS} ${ROOT_SEL} .pin-button,
    html.${TOGGLE_CLASS} ${ROOT_SEL} .more-button {
      display: none !important;
      visibility: hidden !important;
      pointer-events: none !important;
    }

    /* On-screen toggle button */
    .tm-fb-btn {
      position: fixed !important;
      right: 14px;
      bottom: 14px;
      z-index: 2147483647 !important;
      font: 600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
      padding: 10px 12px;
      border-radius: 999px;
      background: rgba(0,0,0,0.6);
      color: #fff;
      border: 1px solid rgba(255,255,255,0.2);
      cursor: pointer;
      user-select: none;
      backdrop-filter: blur(4px);
    }
    .tm-fb-btn:hover { background: rgba(0,0,0,0.75); }
  `);

  // ---------- toggle logic ----------
  const setActive = (on) => {
    const rootList = [document.documentElement, document.body].filter(Boolean);
    rootList.forEach(el => el.classList.toggle(TOGGLE_CLASS, on));
    if (btn) btn.textContent = on ? 'Exit Full-Bleed' : 'Full-Bleed';
  };

  const isActive = () =>
    document.documentElement.classList.contains(TOGGLE_CLASS) ||
    document.body.classList.contains(TOGGLE_CLASS);

  const toggle = () => setActive(!isActive());

  // ---------- button ----------
  let btn;
  const mountButton = () => {
    if (btn && document.body.contains(btn)) return btn;
    btn = document.createElement('button');
    btn.className = 'tm-fb-btn';
    btn.type = 'button';
    btn.textContent = isActive() ? 'Exit Full-Bleed (f)' : 'Full-Bleed (f)';
    btn.addEventListener('click', toggle);
    document.body.appendChild(btn);
    return btn;
  };

  // ---------- event bindings ----------
  const bindKeys = () => {
    window.addEventListener('keydown', (e) => {
      if (e.key === 'f' || e.key === 'F') {
        e.preventDefault();
        toggle();
      } else if (e.key === 'Escape' && isActive()) {
        e.preventDefault();
        setActive(false);
      }
    }, { capture: true });
  };

  const bindVideoDoubleClick = (video) => {
    if (!video || video.dataset.tmFbBound) return;
    video.addEventListener('dblclick', (e) => {
      e.preventDefault();
      toggle();
    }, { capture: true });
    video.dataset.tmFbBound = '1';
  };

  // ---------- observer to reapply on dynamic pages ----------
  const observe = () => {
    const mo = new MutationObserver(() => {
      const video = q(VIDEO_SEL);
      if (video) bindVideoDoubleClick(video);
      if (!btn && document.body) mountButton();
    });
    mo.observe(document.documentElement, { childList: true, subtree: true, attributes: false });
  };

  // ---------- bootstrap ----------
  (async () => {
    await waitFor(() => document.body);
    bindKeys();
    observe();

    const video = await waitFor(VIDEO_SEL, 120, 150);
    if (video) bindVideoDoubleClick(video);

    mountButton();

    // Optional: expose console controls
    window.tmFullBleed = { toggle, on: () => setActive(true), off: () => setActive(false) };
    console.log('[tmFullBleed] Ready on SimplePractice. Use tmFullBleed.toggle(), press F, double-click video, or use the button.');
  })();

})();

See my creation conversation with ChatGPT here:

https://chatgpt.com/share/68c89780-f700-8007-916b-2d707b1970a6

Leave a Reply

Your email address will not be published. Required fields are marked *