{"id":2338,"date":"2026-05-27T05:04:41","date_gmt":"2026-05-27T05:04:41","guid":{"rendered":"https:\/\/420inde.com\/420inde_4.html\/?p=2338"},"modified":"2026-05-27T05:04:42","modified_gmt":"2026-05-27T05:04:42","slug":"2338-2","status":"publish","type":"post","link":"https:\/\/420inde.com\/420inde_4.html\/2338-2\/","title":{"rendered":""},"content":{"rendered":"\n<script data-wp-block-html=\"js\">\n(function () {\n  \"use strict\";\n\n  const DEFAULTS = {\n    channelName: \"TV Mode Channel\",\n    startTime: \"2026-01-01T00:00:00Z\",\n    loop: true,\n    muted: true,\n    autoplay: true,\n    showPlaylist: true,\n    theme: \"#00a0ff\",\n    poster: \"\",\n    playlist: [\n  {\n    title: \"420 Music Video\",\n    src: \"https:\/\/420inde.com\/wp-content\/uploads\/2026\/05\/video1.mp4\",\n    duration: 300\n  },\n  {\n    title: \"420 Podcast\",\n    src: \"https:\/\/420inde.com\/wp-content\/uploads\/2026\/05\/podcast1.mp4\",\n    duration: 600\n  }\n]\n  };\n\n  const styles = `\n    .ctv-shell{--ctv-theme:#00a0ff;box-sizing:border-box;width:100%;max-width:100%;font-family:Inter,Arial,sans-serif;background:#080b10;color:#fff;border-radius:8px;overflow:hidden;box-shadow:0 16px 36px rgba(0,0,0,.24)}\n    .ctv-shell *{box-sizing:border-box}\n    .ctv-stage{position:relative;aspect-ratio:16\/9;background:#000;overflow:hidden}\n    .ctv-video{width:100%;height:100%;display:block;background:#000;object-fit:contain}\n    .ctv-topbar{position:absolute;inset:0 0 auto 0;display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 14px;background:linear-gradient(180deg,rgba(0,0,0,.72),rgba(0,0,0,0));pointer-events:none}\n    .ctv-badge{display:inline-flex;align-items:center;gap:8px;min-width:0;font-size:13px;font-weight:700;letter-spacing:0;text-transform:uppercase}\n    .ctv-dot{width:9px;height:9px;border-radius:50%;background:#fb2c36;box-shadow:0 0 0 4px rgba(251,44,54,.22)}\n    .ctv-now{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;color:rgba(255,255,255,.88);text-align:right}\n    .ctv-center{position:absolute;inset:0;display:grid;place-items:center;pointer-events:none}\n    .ctv-play{pointer-events:auto;width:64px;height:64px;border:0;border-radius:50%;display:grid;place-items:center;background:rgba(0,0,0,.58);color:#fff;cursor:pointer;transition:transform .16s ease,background .16s ease}\n    .ctv-play:hover{transform:scale(1.04);background:rgba(0,0,0,.72)}\n    .ctv-play svg{width:28px;height:28px;margin-left:3px;fill:currentColor}\n    .ctv-controls{position:absolute;inset:auto 0 0 0;padding:20px 12px 12px;background:linear-gradient(0deg,rgba(0,0,0,.82),rgba(0,0,0,0));display:grid;gap:10px}\n    .ctv-progress{height:5px;width:100%;appearance:none;border:0;border-radius:999px;background:rgba(255,255,255,.26);cursor:pointer;accent-color:var(--ctv-theme)}\n    .ctv-progress::-webkit-slider-thumb{appearance:none;width:13px;height:13px;border-radius:50%;background:var(--ctv-theme);box-shadow:0 0 0 3px rgba(255,255,255,.18)}\n    .ctv-row{display:flex;align-items:center;justify-content:space-between;gap:10px}\n    .ctv-left,.ctv-right{display:flex;align-items:center;gap:8px;min-width:0}\n    .ctv-btn{width:36px;height:36px;border:1px solid rgba(255,255,255,.2);border-radius:6px;background:rgba(255,255,255,.08);color:#fff;display:grid;place-items:center;cursor:pointer}\n    .ctv-btn:hover{background:rgba(255,255,255,.16)}\n    .ctv-btn svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}\n    .ctv-time{font-variant-numeric:tabular-nums;font-size:12px;color:rgba(255,255,255,.78);white-space:nowrap}\n    .ctv-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;font-weight:650}\n    .ctv-guide{display:grid;background:#0d1219;border-top:1px solid rgba(255,255,255,.1)}\n    .ctv-guide-head{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:10px 12px;color:rgba(255,255,255,.72);font-size:12px;text-transform:uppercase;font-weight:800;letter-spacing:.06em}\n    .ctv-list{display:grid;max-height:190px;overflow:auto}\n    .ctv-item{display:grid;grid-template-columns:minmax(72px,92px) 1fr auto;gap:10px;align-items:center;padding:10px 12px;border-top:1px solid rgba(255,255,255,.08);color:rgba(255,255,255,.72);font-size:13px}\n    .ctv-item.is-active{color:#fff;background:linear-gradient(90deg,color-mix(in srgb,var(--ctv-theme) 20%,transparent),transparent)}\n    .ctv-item-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:650}\n    .ctv-pill{border:1px solid rgba(255,255,255,.16);border-radius:999px;padding:3px 8px;font-size:11px;color:rgba(255,255,255,.7)}\n    .ctv-error{padding:18px;background:#220b0d;color:#ffd7dc;font-size:14px}\n    @media (max-width:560px){\n      .ctv-topbar{padding:10px}\n      .ctv-now{display:none}\n      .ctv-item{grid-template-columns:64px 1fr}\n      .ctv-pill{display:none}\n      .ctv-title{max-width:46vw}\n    }\n  `;\n\n  function injectStyles() {\n    if (document.getElementById(\"ctv-embed-styles\")) return;\n    const style = document.createElement(\"style\");\n    style.id = \"ctv-embed-styles\";\n    style.textContent = styles;\n    document.head.appendChild(style);\n  }\n\n  function icon(name) {\n    const icons = {\n      play: '<svg viewBox=\"0 0 24 24\"><path d=\"M8 5v14l11-7z\"><\/path><\/svg>',\n      pause: '<svg viewBox=\"0 0 24 24\"><path d=\"M9 5v14M15 5v14\"><\/path><\/svg>',\n      volume: '<svg viewBox=\"0 0 24 24\"><path d=\"M11 5 6 9H3v6h3l5 4V5zM15.5 8.5a5 5 0 0 1 0 7\"><\/path><\/svg>',\n      muted: '<svg viewBox=\"0 0 24 24\"><path d=\"M11 5 6 9H3v6h3l5 4V5zM16 9l5 5M21 9l-5 5\"><\/path><\/svg>',\n      fullscreen: '<svg viewBox=\"0 0 24 24\"><path d=\"M8 3H5a2 2 0 0 0-2 2v3M16 3h3a2 2 0 0 1 2 2v3M8 21H5a2 2 0 0 1-2-2v-3M16 21h3a2 2 0 0 0 2-2v-3\"><\/path><\/svg>'\n    };\n    return icons[name] || \"\";\n  }\n\n  function formatTime(seconds) {\n    const total = Math.max(0, Math.floor(seconds || 0));\n    const h = Math.floor(total \/ 3600);\n    const m = Math.floor((total % 3600) \/ 60);\n    const s = total % 60;\n    return h > 0\n      ? `${h}:${String(m).padStart(2, \"0\")}:${String(s).padStart(2, \"0\")}`\n      : `${m}:${String(s).padStart(2, \"0\")}`;\n  }\n\n  function parseConfig(root) {\n    const jsonNode = root.querySelector('script[type=\"application\/json\"]');\n    const data = {};\n    if (jsonNode && jsonNode.textContent.trim()) {\n      try {\n        Object.assign(data, JSON.parse(jsonNode.textContent));\n      } catch (error) {\n        throw new Error(\"The TV Mode JSON config is not valid JSON.\");\n      }\n    }\n\n    return {\n      ...DEFAULTS,\n      ...data,\n      loop: data.loop !== undefined ? Boolean(data.loop) : DEFAULTS.loop,\n      muted: data.muted !== undefined ? Boolean(data.muted) : DEFAULTS.muted,\n      autoplay: data.autoplay !== undefined ? Boolean(data.autoplay) : DEFAULTS.autoplay,\n      showPlaylist: data.showPlaylist !== undefined ? Boolean(data.showPlaylist) : DEFAULTS.showPlaylist\n    };\n  }\n\n  function normalizePlaylist(config) {\n    const playlist = Array.isArray(config.playlist) ? config.playlist : [];\n    return playlist\n      .map((item, index) => ({\n        title: item.title || `Program ${index + 1}`,\n        src: item.src || \"\",\n        type: item.type || \"\",\n        duration: Number(item.duration || 0),\n        poster: item.poster || config.poster || \"\"\n      }))\n      .filter((item) => item.src && item.duration > 0);\n  }\n\n  function getSchedulePosition(config, playlist) {\n    const totalDuration = playlist.reduce((sum, item) => sum + item.duration, 0);\n    if (!totalDuration) return { index: 0, offset: 0, elapsed: 0, totalDuration: 0 };\n\n    const startMs = Date.parse(config.startTime) || Date.now();\n    const elapsed = Math.max(0, Math.floor((Date.now() - startMs) \/ 1000));\n    let cursor = config.loop ? elapsed % totalDuration : Math.min(elapsed, totalDuration - 1);\n\n    for (let index = 0; index < playlist.length; index += 1) {\n      if (cursor < playlist[index].duration) {\n        return { index, offset: cursor, elapsed, totalDuration };\n      }\n      cursor -= playlist[index].duration;\n    }\n\n    return { index: playlist.length - 1, offset: 0, elapsed, totalDuration };\n  }\n\n  function createPlayer(root, config, playlist) {\n    root.innerHTML = `\n      <div class=\"ctv-shell\" style=\"--ctv-theme:${config.theme}\">\n        <div class=\"ctv-stage\">\n          <video class=\"ctv-video\" playsinline ${config.muted ? \"muted\" : \"\"} ${config.poster ? `poster=\"${config.poster}\"` : \"\"}><\/video>\n          <div class=\"ctv-topbar\">\n            <div class=\"ctv-badge\"><span class=\"ctv-dot\"><\/span><span>Live<\/span><\/div>\n            <div class=\"ctv-now\"><\/div>\n          <\/div>\n          <div class=\"ctv-center\"><button class=\"ctv-play\" type=\"button\" aria-label=\"Play\">${icon(\"play\")}<\/button><\/div>\n          <div class=\"ctv-controls\">\n            <input class=\"ctv-progress\" type=\"range\" min=\"0\" max=\"1000\" value=\"0\" aria-label=\"Program progress\">\n            <div class=\"ctv-row\">\n              <div class=\"ctv-left\">\n                <button class=\"ctv-btn ctv-toggle\" type=\"button\" aria-label=\"Play or pause\">${icon(\"play\")}<\/button>\n                <button class=\"ctv-btn ctv-mute\" type=\"button\" aria-label=\"Mute or unmute\">${icon(config.muted ? \"muted\" : \"volume\")}<\/button>\n                <span class=\"ctv-time\">0:00 \/ 0:00<\/span>\n              <\/div>\n              <div class=\"ctv-right\">\n                <span class=\"ctv-title\"><\/span>\n                <button class=\"ctv-btn ctv-fullscreen\" type=\"button\" aria-label=\"Fullscreen\">${icon(\"fullscreen\")}<\/button>\n              <\/div>\n            <\/div>\n          <\/div>\n        <\/div>\n        ${config.showPlaylist ? '<div class=\"ctv-guide\"><div class=\"ctv-guide-head\"><span>Channel Guide<\/span><span class=\"ctv-loop-label\"><\/span><\/div><div class=\"ctv-list\"><\/div><\/div>' : \"\"}\n      <\/div>\n    `;\n\n    const shell = root.querySelector(\".ctv-shell\");\n    const video = root.querySelector(\".ctv-video\");\n    const centerPlay = root.querySelector(\".ctv-play\");\n    const toggle = root.querySelector(\".ctv-toggle\");\n    const mute = root.querySelector(\".ctv-mute\");\n    const fullscreen = root.querySelector(\".ctv-fullscreen\");\n    const progress = root.querySelector(\".ctv-progress\");\n    const time = root.querySelector(\".ctv-time\");\n    const title = root.querySelector(\".ctv-title\");\n    const now = root.querySelector(\".ctv-now\");\n    const list = root.querySelector(\".ctv-list\");\n    const loopLabel = root.querySelector(\".ctv-loop-label\");\n    let currentIndex = -1;\n    let seeking = false;\n\n    if (loopLabel) loopLabel.textContent = config.loop ? \"Looping\" : \"Scheduled\";\n\n    function renderGuide() {\n      if (!list) return;\n      let running = 0;\n      list.innerHTML = playlist.map((item, index) => {\n        const starts = formatTime(running);\n        running += item.duration;\n        return `\n          <div class=\"ctv-item\" data-index=\"${index}\">\n            <span>${starts}<\/span>\n            <span class=\"ctv-item-title\">${item.title}<\/span>\n            <span class=\"ctv-pill\">${formatTime(item.duration)}<\/span>\n          <\/div>\n        `;\n      }).join(\"\");\n    }\n\n    function highlightGuide() {\n      if (!list) return;\n      list.querySelectorAll(\".ctv-item\").forEach((item) => {\n        item.classList.toggle(\"is-active\", Number(item.dataset.index) === currentIndex);\n      });\n    }\n\n    function setSource(index, offset) {\n      const item = playlist[index];\n      currentIndex = index;\n      video.src = item.src;\n      video.type = item.type;\n      if (item.poster) video.poster = item.poster;\n      title.textContent = item.title;\n      now.textContent = `${config.channelName}: ${item.title}`;\n      highlightGuide();\n\n      video.addEventListener(\"loadedmetadata\", function onLoaded() {\n        video.removeEventListener(\"loadedmetadata\", onLoaded);\n        try {\n          video.currentTime = Math.min(offset || 0, Math.max(0, video.duration - 0.25));\n        } catch (error) {\n          video.currentTime = 0;\n        }\n        if (config.autoplay) video.play().catch(() => {});\n      });\n\n      video.load();\n    }\n\n    function syncToClock() {\n      const pos = getSchedulePosition(config, playlist);\n      if (pos.index !== currentIndex) {\n        setSource(pos.index, pos.offset);\n        return;\n      }\n\n      const drift = Math.abs(video.currentTime - pos.offset);\n      if (!video.paused && drift > 4) {\n        video.currentTime = pos.offset;\n      }\n    }\n\n    function playNext() {\n      const nextIndex = currentIndex + 1;\n      if (nextIndex < playlist.length) {\n        setSource(nextIndex, 0);\n      } else if (config.loop) {\n        setSource(0, 0);\n      } else {\n        video.pause();\n      }\n    }\n\n    function updateControls() {\n      const item = playlist[currentIndex] || playlist[0];\n      const duration = item ? item.duration : video.duration || 0;\n      const current = Math.min(video.currentTime || 0, duration);\n      if (!seeking) progress.value = duration ? String((current \/ duration) * 1000) : \"0\";\n      time.textContent = `${formatTime(current)} \/ ${formatTime(duration)}`;\n      const isPlaying = !video.paused &#038;&#038; !video.ended;\n      toggle.innerHTML = icon(isPlaying ? \"pause\" : \"play\");\n      centerPlay.style.display = isPlaying ? \"none\" : \"grid\";\n      centerPlay.innerHTML = icon(\"play\");\n      mute.innerHTML = icon(video.muted ? \"muted\" : \"volume\");\n    }\n\n    centerPlay.addEventListener(\"click\", () => video.play().catch(() => {}));\n    toggle.addEventListener(\"click\", () => {\n      if (video.paused) video.play().catch(() => {});\n      else video.pause();\n    });\n    mute.addEventListener(\"click\", () => {\n      video.muted = !video.muted;\n      updateControls();\n    });\n    fullscreen.addEventListener(\"click\", () => {\n      if (document.fullscreenElement) document.exitFullscreen();\n      else shell.requestFullscreen?.();\n    });\n    progress.addEventListener(\"input\", () => {\n      seeking = true;\n      const item = playlist[currentIndex] || playlist[0];\n      const duration = item ? item.duration : video.duration || 0;\n      time.textContent = `${formatTime((Number(progress.value) \/ 1000) * duration)} \/ ${formatTime(duration)}`;\n    });\n    progress.addEventListener(\"change\", () => {\n      const item = playlist[currentIndex] || playlist[0];\n      const duration = item ? item.duration : video.duration || 0;\n      video.currentTime = (Number(progress.value) \/ 1000) * duration;\n      seeking = false;\n    });\n    video.addEventListener(\"timeupdate\", updateControls);\n    video.addEventListener(\"play\", updateControls);\n    video.addEventListener(\"pause\", updateControls);\n    video.addEventListener(\"ended\", playNext);\n    video.addEventListener(\"error\", () => {\n      now.textContent = \"This program could not be loaded.\";\n    });\n\n    renderGuide();\n    syncToClock();\n    updateControls();\n    setInterval(syncToClock, 15000);\n    setInterval(updateControls, 500);\n  }\n\n  function initOne(root) {\n    try {\n      const config = parseConfig(root);\n      const playlist = normalizePlaylist(config);\n      if (!playlist.length) {\n        root.innerHTML = '<div class=\"ctv-error\">Add at least one playlist item with src and duration.<\/div>';\n        return;\n      }\n      createPlayer(root, config, playlist);\n    } catch (error) {\n      root.innerHTML = `<div class=\"ctv-error\">${error.message}<\/div>`;\n    }\n  }\n\n  function init() {\n    injectStyles();\n    document.querySelectorAll(\"[data-castr-tv-mode]:not([data-castr-tv-ready])\").forEach((root) => {\n      root.setAttribute(\"data-castr-tv-ready\", \"true\");\n      initOne(root);\n    });\n  }\n\n  window.CastrTVModeEmbed = { init };\n\n  if (document.readyState === \"loading\") {\n    document.addEventListener(\"DOMContentLoaded\", init);\n  } else {\n    init();\n  }\n})();\n<\/script>\n\n<meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>Castr-Style TV Mode Embed Demo<\/title>\n    <style>\n      body {\n        margin: 0;\n        min-height: 100vh;\n        display: grid;\n        place-items: center;\n        padding: 24px;\n        background: #f3f6fa;\n        color: #121821;\n        font-family: Arial, sans-serif;\n      }\n\n      main {\n        width: min(980px, 100%);\n      }\n\n      h1 {\n        margin: 0 0 16px;\n        font-size: clamp(24px, 4vw, 42px);\n        letter-spacing: 0;\n      }\n\n      p {\n        margin: 0 0 18px;\n        color: #4c5867;\n        line-height: 1.5;\n      }\n    <\/style>\n  \n  \n    <main>\n      <h1>TV Mode Embed Demo<\/h1>\n      <p>This player starts at the current point in a repeating schedule, like a simple 24\/7 linear channel.<\/p>\n\n      <div data-castr-tv-mode=\"\">\n        <script type=\"application\/json\">\n          {\n            \"channelName\": \"Demo Channel\",\n            \"startTime\": \"2026-01-01T00:00:00Z\",\n            \"loop\": true,\n            \"muted\": true,\n            \"autoplay\": true,\n            \"showPlaylist\": true,\n            \"theme\": \"#0ea5e9\",\n            \"playlist\": [\n              {\n                \"title\": \"Opening Feature\",\n                \"src\": \"https:\/\/interactive-examples.mdn.mozilla.net\/media\/cc0-videos\/flower.mp4\",\n                \"duration\": 5\n              },\n              {\n                \"title\": \"Second Program\",\n                \"src\": \"https:\/\/www.w3schools.com\/html\/mov_bbb.mp4\",\n                \"duration\": 10\n              },\n              {\n                \"title\": \"Channel Interlude\",\n                \"src\": \"https:\/\/interactive-examples.mdn.mozilla.net\/media\/cc0-videos\/flower.mp4\",\n                \"duration\": 8\n              }\n            ]\n          }\n        <\/script>\n      <\/div>\n    <\/main>\n\n    <script src=\".\/castr-tv-mode-embed.js\"><\/script>\n","protected":false},"excerpt":{"rendered":"<p>Castr-Style TV Mode Embed Demo TV Mode Embed Demo This player starts at the current point in a repeating schedule, like a simple 24\/7 linear channel.<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[34],"tags":[],"class_list":["post-2338","post","type-post","status-publish","format-standard","hentry","category-the420-video-tv"],"acf":[],"_links":{"self":[{"href":"https:\/\/420inde.com\/420inde_4.html\/wp-json\/wp\/v2\/posts\/2338","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/420inde.com\/420inde_4.html\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/420inde.com\/420inde_4.html\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/420inde.com\/420inde_4.html\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/420inde.com\/420inde_4.html\/wp-json\/wp\/v2\/comments?post=2338"}],"version-history":[{"count":1,"href":"https:\/\/420inde.com\/420inde_4.html\/wp-json\/wp\/v2\/posts\/2338\/revisions"}],"predecessor-version":[{"id":2339,"href":"https:\/\/420inde.com\/420inde_4.html\/wp-json\/wp\/v2\/posts\/2338\/revisions\/2339"}],"wp:attachment":[{"href":"https:\/\/420inde.com\/420inde_4.html\/wp-json\/wp\/v2\/media?parent=2338"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/420inde.com\/420inde_4.html\/wp-json\/wp\/v2\/categories?post=2338"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/420inde.com\/420inde_4.html\/wp-json\/wp\/v2\/tags?post=2338"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}