{"id":12,"date":"2020-05-17T02:28:04","date_gmt":"2020-05-17T02:28:04","guid":{"rendered":"http:\/\/www.antoniusfeast.com\/?page_id=12"},"modified":"2026-04-02T18:11:52","modified_gmt":"2026-04-02T18:11:52","slug":"live-stream","status":"publish","type":"page","link":"https:\/\/www.antonius.org\/","title":{"rendered":"Live Stream"},"content":{"rendered":"\n<p><\/p>\n\n\n\n<!-- ============================================================\n     YouTube \"Next Scheduled Event\" Widget\n     Channel: @stantoniusvideo  (UCyAkAMVD9xvvydk802nABew)\n     \u25b8 Replace YOUR_API_KEY with your YouTube Data API v3 key\n     ============================================================ -->\n\n<div id=\"yt-next-event\"><\/div>\n\n<style>\n  #yt-next-event {\n    font-family: \"Roboto\", Arial, sans-serif;\n    max-width: 400px;\n  }\n\n  .yt-card {\n    background: #0f0f0f;\n    border-radius: 12px;\n    overflow: hidden;\n    color: #fff;\n    box-shadow: 0 4px 24px rgba(0,0,0,.45);\n  }\n\n  .yt-thumb-wrap {\n    position: relative;\n    width: 100%;\n    aspect-ratio: 16\/9;\n    background: #1a1a1a;\n    overflow: hidden;\n  }\n\n  .yt-thumb-wrap img {\n    width: 100%;\n    height: 100%;\n    object-fit: cover;\n    display: block;\n  }\n\n  .yt-badge {\n    position: absolute;\n    top: 10px;\n    left: 10px;\n    background: #ff0000;\n    color: #fff;\n    font-size: 11px;\n    font-weight: 700;\n    letter-spacing: .6px;\n    text-transform: uppercase;\n    padding: 3px 8px;\n    border-radius: 4px;\n  }\n\n  .yt-countdown {\n    position: absolute;\n    bottom: 10px;\n    right: 10px;\n    background: rgba(0,0,0,.75);\n    color: #fff;\n    font-size: 12px;\n    font-weight: 600;\n    padding: 4px 10px;\n    border-radius: 4px;\n    letter-spacing: .3px;\n  }\n\n  .yt-body {\n    padding: 14px 16px 16px;\n  }\n\n  .yt-channel-row {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    margin-bottom: 10px;\n  }\n\n  .yt-avatar {\n    width: 36px;\n    height: 36px;\n    border-radius: 50%;\n    object-fit: cover;\n    flex-shrink: 0;\n    background: #333;\n  }\n\n  .yt-channel-name {\n    font-size: 13px;\n    color: #aaa;\n  }\n\n  .yt-title {\n    font-size: 15px;\n    font-weight: 600;\n    line-height: 1.4;\n    color: #f1f1f1;\n    margin: 0 0 10px;\n    display: -webkit-box;\n    -webkit-line-clamp: 2;\n    -webkit-box-orient: vertical;\n    overflow: hidden;\n  }\n\n  .yt-meta {\n    font-size: 12px;\n    color: #aaa;\n    margin-bottom: 14px;\n  }\n\n  .yt-btn-row {\n    display: flex;\n    gap: 8px;\n    flex-wrap: wrap;\n  }\n\n  .yt-btn {\n    display: inline-flex;\n    align-items: center;\n    gap: 6px;\n    padding: 8px 16px;\n    border-radius: 20px;\n    font-size: 13px;\n    font-weight: 600;\n    cursor: pointer;\n    text-decoration: none;\n    border: none;\n    transition: filter .15s;\n  }\n  .yt-btn:hover { filter: brightness(1.15); }\n\n  .yt-btn-primary {\n    background: #ff0000;\n    color: #fff;\n  }\n\n  .yt-badge-live {\n    background: #ff0000;\n    animation: yt-pulse 1.5s infinite;\n  }\n\n  @keyframes yt-pulse {\n    0%, 100% { opacity: 1; }\n    50%       { opacity: .6; }\n  }\n\n  .yt-btn-live {\n    background: #ff0000;\n    color: #fff;\n  }\n\n  .yt-btn-secondary {\n    background: #272727;\n    color: #fff;\n  }\n\n  .yt-none {\n    color: #aaa;\n    font-size: 14px;\n    padding: 16px 0;\n    font-family: \"Roboto\", Arial, sans-serif;\n  }\n\n  .yt-error {\n    color: #f44;\n    font-size: 13px;\n    font-family: \"Roboto\", Arial, sans-serif;\n    padding: 12px 0;\n  }\n<\/style>\n\n<script>\n(function () {\n  const API_KEY    = \"AIzaSyCv8vmD2Tijy5fnV1Lp1H390Ix1mcK9ZeQ\";\n  const CHANNEL_ID = \"UCyAkAMVD9xvvydk802nABew\";\n  const HANDLE     = \"@stantoniusvideo\";\n  const BASE       = \"https:\/\/www.googleapis.com\/youtube\/v3\";\n  const container  = document.getElementById(\"yt-next-event\");\n\n  \/* \u2500\u2500 helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n\n  function formatDate(iso) {\n    const d = new Date(iso);\n    return d.toLocaleDateString(undefined, {\n      weekday: \"long\", month: \"long\", day: \"numeric\",\n      hour: \"2-digit\", minute: \"2-digit\"\n    });\n  }\n\n  function countdown(iso) {\n    const diff = new Date(iso) - Date.now();\n    if (diff <= 0) return \"Starting soon\";\n    const h = Math.floor(diff \/ 36e5);\n    const m = Math.floor((diff % 36e5) \/ 6e4);\n    if (h >= 24) {\n      const days = Math.floor(h \/ 24);\n      return `in ${days} day${days > 1 ? \"s\" : \"\"}`;\n    }\n    if (h > 0) return `in ${h}h ${m}m`;\n    return `in ${m}m`;\n  }\n\n  \/* \u2500\u2500 fetch channel avatar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n\n  async function fetchAvatar() {\n    try {\n      const r = await fetch(`${BASE}\/channels?part=snippet&id=${CHANNEL_ID}&key=${API_KEY}`);\n      const d = await r.json();\n      return d.items?.[0]?.snippet?.thumbnails?.default?.url || null;\n    } catch { return null; }\n  }\n\n  \/* \u2500\u2500 fetch best thumbnail \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n\n  function bestThumb(thumbnails, videoId) {\n    const t = thumbnails || {};\n    return (t.maxres || t.standard || t.high || t.medium || t.default)?.url\n      || `https:\/\/i.ytimg.com\/vi\/${videoId}\/hqdefault.jpg`;\n  }\n\n  \/* \u2500\u2500 check for live stream then fall back to upcoming \u2500\u2500 *\/\n\n  async function fetchNextEvent() {\n    container.innerHTML = `<div class=\"yt-none\">Loading\u2026<\/div>`;\n    try {\n      \/\/ Fetch avatar + check live simultaneously\n      const [avatar, liveSearch] = await Promise.all([\n        fetchAvatar(),\n        fetch(\n          `${BASE}\/search?part=snippet&channelId=${CHANNEL_ID}` +\n          `&eventType=live&type=video&maxResults=1&key=${API_KEY}`\n        ).then(r => r.json())\n      ]);\n\n      \/\/ \u2500\u2500 LIVE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n      if (liveSearch.items && liveSearch.items.length > 0) {\n        const liveId = liveSearch.items[0].id.videoId;\n\n        \/\/ Get full details including actual viewer count & real thumbnail\n        const liveDetails = await fetch(\n          `${BASE}\/videos?part=snippet,liveStreamingDetails&id=${liveId}&key=${API_KEY}`\n        ).then(r => r.json());\n\n        const v       = liveDetails.items?.[0];\n        const viewers = v?.liveStreamingDetails?.concurrentViewers\n          ? Number(v.liveStreamingDetails.concurrentViewers).toLocaleString()\n          : null;\n        const thumb   = bestThumb(v?.snippet?.thumbnails, liveId);\n        const title   = v?.snippet?.title || liveSearch.items[0].snippet.title;\n        const channel = v?.snippet?.channelTitle || liveSearch.items[0].snippet.channelTitle;\n\n        renderLive(liveId, title, channel, avatar, thumb, viewers);\n        return;\n      }\n\n      \/\/ \u2500\u2500 UPCOMING \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n      const upcomingSearch = await fetch(\n        `${BASE}\/search?part=snippet&channelId=${CHANNEL_ID}` +\n        `&eventType=upcoming&type=video&maxResults=10&key=${API_KEY}`\n      ).then(r => r.json());\n\n      if (!upcomingSearch.items || upcomingSearch.items.length === 0) {\n        container.innerHTML = `<div class=\"yt-none\">No upcoming events scheduled right now.<\/div>`;\n        return;\n      }\n\n      const ids      = upcomingSearch.items.map(v => v.id.videoId).join(\",\");\n      const videoRes = await fetch(\n        `${BASE}\/videos?part=snippet,liveStreamingDetails&id=${ids}&key=${API_KEY}`\n      ).then(r => r.json());\n\n      if (!videoRes.items || videoRes.items.length === 0) {\n        container.innerHTML = `<div class=\"yt-none\">No upcoming events scheduled right now.<\/div>`;\n        return;\n      }\n\n      const now = Date.now();\n\n      \/\/ Split into two groups: events with a known future start time, and everything else\n      const withTime    = videoRes.items.filter(v => v.liveStreamingDetails?.scheduledStartTime);\n      const futureItems = withTime.filter(v => new Date(v.liveStreamingDetails.scheduledStartTime).getTime() > now);\n      const noTime      = videoRes.items.filter(v => !v.liveStreamingDetails?.scheduledStartTime);\n\n      \/\/ Sort timed events by start time, then append any without a time as fallbacks\n      futureItems.sort((a, b) =>\n        new Date(a.liveStreamingDetails.scheduledStartTime) -\n        new Date(b.liveStreamingDetails.scheduledStartTime)\n      );\n\n      const upcoming = [...futureItems, ...noTime];\n\n      if (upcoming.length === 0) {\n        container.innerHTML = `<div class=\"yt-none\">No upcoming events scheduled right now.<\/div>`;\n        return;\n      }\n\n      const next  = upcoming[0];\n      const start = next.liveStreamingDetails?.scheduledStartTime || null;\n      const thumb = bestThumb(next.snippet.thumbnails, next.id);\n      renderUpcoming(next.id, next.snippet.title, start, next.snippet.channelTitle, avatar, thumb);\n\n    } catch (err) {\n      container.innerHTML =\n        `<div class=\"yt-error\">\u26a0\ufe0f Could not load: ${err.message}<\/div>`;\n    }\n  }\n\n  \/* \u2500\u2500 render: LIVE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n\n  function renderLive(videoId, title, channelTitle, channelAvatar, thumbnail, viewers) {\n    const videoUrl  = `https:\/\/www.youtube.com\/watch?v=${videoId}`;\n\n    container.innerHTML = `\n      <div class=\"yt-card\">\n        <div class=\"yt-thumb-wrap\">\n          <img decoding=\"async\" src=\"${thumbnail}\"\n               onerror=\"this.src='https:\/\/i.ytimg.com\/vi\/${videoId}\/hqdefault.jpg'\"\n               alt=\"${title}\">\n          <span class=\"yt-badge yt-badge-live\">\u25cf LIVE NOW<\/span>\n          ${viewers ? `<span class=\"yt-countdown\">\ud83d\udc41 ${viewers} watching<\/span>` : \"\"}\n        <\/div>\n        <div class=\"yt-body\">\n          <div class=\"yt-channel-row\">\n            ${channelAvatar\n              ? `<img decoding=\"async\" class=\"yt-avatar\" src=\"${channelAvatar}\" alt=\"${HANDLE}\">`\n              : \"\"}\n            <span class=\"yt-channel-name\">${channelTitle}<\/span>\n          <\/div>\n          <p class=\"yt-title\">${title}<\/p>\n          <div class=\"yt-btn-row\">\n            <a class=\"yt-btn yt-btn-live\" href=\"${videoUrl}\" target=\"_blank\" rel=\"noopener\">\n              \u25cf Watch Live\n            <\/a>\n          <\/div>\n        <\/div>\n      <\/div>`;\n\n\n  }\n\n  \/* \u2500\u2500 render: UPCOMING \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n\n  function renderUpcoming(videoId, title, start, channelTitle, channelAvatar, thumbnail) {\n    const videoUrl  = `https:\/\/www.youtube.com\/watch?v=${videoId}`;\n    const notifyUrl = `https:\/\/www.youtube.com\/channel\/${CHANNEL_ID}?sub_confirmation=1`;\n\n    container.innerHTML = `\n      <div class=\"yt-card\">\n        <div class=\"yt-thumb-wrap\">\n          <img decoding=\"async\" src=\"${thumbnail}\"\n               onerror=\"this.src='https:\/\/i.ytimg.com\/vi\/${videoId}\/hqdefault.jpg'\"\n               alt=\"${title}\">\n          <span class=\"yt-badge\">\ud83d\udd34 Upcoming<\/span>\n          ${start ? `<span class=\"yt-countdown\">${countdown(start)}<\/span>` : \"\"}\n        <\/div>\n        <div class=\"yt-body\">\n          <div class=\"yt-channel-row\">\n            ${channelAvatar\n              ? `<img decoding=\"async\" class=\"yt-avatar\" src=\"${channelAvatar}\" alt=\"${HANDLE}\">`\n              : \"\"}\n            <span class=\"yt-channel-name\">${channelTitle}<\/span>\n          <\/div>\n          <p class=\"yt-title\">${title}<\/p>\n          ${start ? `<div class=\"yt-meta\">\ud83d\udcc5 ${formatDate(start)}<\/div>` : \"\"}\n          <div class=\"yt-btn-row\">\n            <a class=\"yt-btn yt-btn-primary\" href=\"${videoUrl}\" target=\"_blank\" rel=\"noopener\">\n              \u25b6 Watch\n            <\/a>\n            <a class=\"yt-btn yt-btn-secondary\" href=\"${notifyUrl}\" target=\"_blank\" rel=\"noopener\">\n              \ud83d\udd14 Notify me\n            <\/a>\n          <\/div>\n        <\/div>\n      <\/div>`;\n\n    \/\/ Live countdown ticker\n    if (start) {\n      setInterval(() => {\n        const el = container.querySelector(\".yt-countdown\");\n        if (el) el.textContent = countdown(start);\n      }, 30_000);\n    }\n\n\n  }\n\n  fetchNextEvent();\n})();\n<\/script>\n\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":229,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-12","page","type-page","status-publish","has-post-thumbnail","hentry"],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.antonius.org\/index.php?rest_route=\/wp\/v2\/pages\/12","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.antonius.org\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.antonius.org\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.antonius.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.antonius.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=12"}],"version-history":[{"count":17,"href":"https:\/\/www.antonius.org\/index.php?rest_route=\/wp\/v2\/pages\/12\/revisions"}],"predecessor-version":[{"id":1006,"href":"https:\/\/www.antonius.org\/index.php?rest_route=\/wp\/v2\/pages\/12\/revisions\/1006"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.antonius.org\/index.php?rest_route=\/wp\/v2\/media\/229"}],"wp:attachment":[{"href":"https:\/\/www.antonius.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=12"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}