|
<html> |
|
<head> |
|
<title>🤖 AI-TV</title> |
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/full.css" rel="stylesheet" type="text/css" /> |
|
|
|
<script src="/mpegts.js"></script> |
|
</head> |
|
<body |
|
x-data="app()" x-init="init()" |
|
class="fixed inset-0 bg-[rgb(0,0,0)] flex flex-col w-full items-center justify-start"> |
|
<div x-show="!enabled">Loading AI-TV...</div> |
|
|
|
<div |
|
x-show="enabled && showToolbar" |
|
x-transition:enter="transition ease-out duration-100" |
|
x-transition:enter-start="opacity-0 -translate-y-8" |
|
x-transition:enter-end="opacity-100" |
|
x-transition:leave="transition ease-in duration-200" |
|
x-transition:leave-start="opacity-100" |
|
x-transition:leave-end="opacity-0 -translate-y-8" |
|
class="fixed w-full z-20 py-4 px-6 top-0 font-mono text-white flex flex-col lg:flex-row items-center justify-between space-x-1 bg-black bg-opacity-60" |
|
style="text-shadow: 0px 0px 3px #000000"> |
|
|
|
<div class="flex text-xl space-x-2"> |
|
<div class="text-xl">🤖 AI-TV</div> |
|
<div class="text-md">▶ Current channel:</div> |
|
<template x-for="chan in channels"> |
|
<div |
|
class="text-xl mr-2" |
|
:class="chan.id === channel.id |
|
? 'font-bold' |
|
: 'hover:underline opacity-60 hover:opacity-80 cursor-pointer'" |
|
x-on:click="window.location = `${window.location.origin}/?channel=${chan.id}`" |
|
x-text="chan.label"> |
|
<div class="animate-ping absolute inline-flex h-4 w-4 rounded-full bg-red-400 opacity-75"></div> |
|
</div> |
|
</template> |
|
</div> |
|
|
|
<div class="flex justify-between space-x-6 items-center"> |
|
|
|
<div class="flex items-center justify-center text-white opacity-100 space-x-2"> |
|
<div> |
|
<svg xmlns="http://www.w3.org/2000/svg" width="24px" height="24px" viewBox="0 0 640 512"><path fill="currentColor" d="M96 128a128 128 0 1 1 256 0A128 128 0 1 1 96 128zM0 482.3C0 383.8 79.8 304 178.3 304h91.4C368.2 304 448 383.8 448 482.3c0 16.4-13.3 29.7-29.7 29.7H29.7C13.3 512 0 498.7 0 482.3zM609.3 512H471.4c5.4-9.4 8.6-20.3 8.6-32v-8c0-60.7-27.1-115.2-69.8-151.8c2.4-.1 4.7-.2 7.1-.2h61.4C567.8 320 640 392.2 640 481.3c0 17-13.8 30.7-30.7 30.7zM432 256c-31 0-59-12.6-79.3-32.9C372.4 196.5 384 163.6 384 128c0-26.8-6.6-52.1-18.3-74.3C384.3 40.1 407.2 32 432 32c61.9 0 112 50.1 112 112s-50.1 112-112 112z"/></svg> |
|
</div> |
|
<div x-text="channel.audience"></div> |
|
<div x-text="channel.audience > 1 ? 'viewers' : '🟢 Online'"></div> |
|
</div> |
|
|
|
<div class="text-sm">(<a |
|
class="hover:underline" |
|
href="https://huggingface.co/facebook/musicgen-melody" |
|
target="_blank">musicgen-melody</a> + <a |
|
class="hover:underline" |
|
:href="channel.modelUrl" |
|
x-text="channel.model" |
|
target="_blank"></a>)</div> |
|
|
|
<div |
|
x-on:click="toggleAudio()" |
|
class="flex items-center justify-center text-white opacity-80 hover:opacity-100 cursor-pointer"> |
|
<div x-show="muted"> |
|
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="32px" height="32px"><path fill="currentColor" d="M215.03 71.05L126.06 160H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V88.02c0-21.46-25.96-31.98-40.97-16.97zM461.64 256l45.64-45.64c6.3-6.3 6.3-16.52 0-22.82l-22.82-22.82c-6.3-6.3-16.52-6.3-22.82 0L416 210.36l-45.64-45.64c-6.3-6.3-16.52-6.3-22.82 0l-22.82 22.82c-6.3 6.3-6.3 16.52 0 22.82L370.36 256l-45.63 45.63c-6.3 6.3-6.3 16.52 0 22.82l22.82 22.82c6.3 6.3 16.52 6.3 22.82 0L416 301.64l45.64 45.64c6.3 6.3 16.52 6.3 22.82 0l22.82-22.82c6.3-6.3 6.3-16.52 0-22.82L461.64 256z" class=""></path></svg> |
|
</div> |
|
<div x-show="!muted"> |
|
<svg aria-hidden="true" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 480 512" width="32px" height="32px"><path fill="currentColor" d="M215.03 71.05L126.06 160H24c-13.26 0-24 10.74-24 24v144c0 13.25 10.74 24 24 24h102.06l88.97 88.95c15.03 15.03 40.97 4.47 40.97-16.97V88.02c0-21.46-25.96-31.98-40.97-16.97zM480 256c0-63.53-32.06-121.94-85.77-156.24-11.19-7.14-26.03-3.82-33.12 7.46s-3.78 26.21 7.41 33.36C408.27 165.97 432 209.11 432 256s-23.73 90.03-63.48 115.42c-11.19 7.14-14.5 22.07-7.41 33.36 6.51 10.36 21.12 15.14 33.12 7.46C447.94 377.94 480 319.53 480 256zm-141.77-76.87c-11.58-6.33-26.19-2.16-32.61 9.45-6.39 11.61-2.16 26.2 9.45 32.61C327.98 228.28 336 241.63 336 256c0 14.38-8.02 27.72-20.92 34.81-11.61 6.41-15.84 21-9.45 32.61 6.43 11.66 21.05 15.8 32.61 9.45 28.23-15.55 45.77-45 45.77-76.88s-17.54-61.32-45.78-76.86z" class=""></path></svg> |
|
</div> |
|
</div> |
|
<div |
|
x-on:click="fullscreen()" |
|
class="text-white hover:text-white opacity-80 hover:opacity-100 cursor-pointer"> |
|
<?xml version="1.0" ?><svg version="1.1" viewBox="0 0 14 14" width="24px" height="24px" xmlns="http://www.w3.org/2000/svg" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns" xmlns:xlink="http://www.w3.org/1999/xlink"><title/><desc/><defs/><g fill="none" fill-rule="evenodd" id="Page-1" stroke="none" stroke-width="1"><g fill="currentColor" id="Core" transform="translate(-215.000000, -257.000000)"><g id="fullscreen" transform="translate(215.000000, 257.000000)"><path d="M2,9 L0,9 L0,14 L5,14 L5,12 L2,12 L2,9 L2,9 Z M0,5 L2,5 L2,2 L5,2 L5,0 L0,0 L0,5 L0,5 Z M12,12 L9,12 L9,14 L14,14 L14,9 L12,9 L12,12 L12,12 Z M9,0 L9,2 L12,2 L12,5 L14,5 L14,0 L9,0 L9,0 Z" id="Shape"/></g></g></g></svg> |
|
</div> |
|
</div> |
|
</div> |
|
<div class="flex w-full"> |
|
<video id="videoElement" muted autoplay class="aspect-video w-full"></video> |
|
|
|
|
|
|
|
|
|
</div> |
|
<script> |
|
|
|
window.HELP_IMPROVE_VIDEOJS = false |
|
</script> |
|
<script defer src="https://cdn.jsdelivr.net/npm/[email protected]/dist/cdn.min.js"></script> |
|
<script src="https://cdn.tailwindcss.com?plugins=forms,typography,aspect-ratio"></script> |
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.2/iframeResizer.contentWindow.min.js"></script> |
|
|
|
<script> |
|
|
|
function app() { |
|
return { |
|
enabled: false, |
|
channels: { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
random: { |
|
id: 'random', |
|
label: 'All', |
|
audience: 0, |
|
online: false, |
|
visible: true, |
|
url: 'https://jbilcke-hf-media-server.hf.space/live/random.flv', |
|
resolution: '1024x576_24FPS', |
|
model: 'zeroscope_v2_XL', |
|
modelUrl: 'https://huggingface.co/cerspense/zeroscope_v2_XL', |
|
}, |
|
comedy: { |
|
id: 'comedy', |
|
label: 'Comedy', |
|
audience: 0, |
|
online: false, |
|
visible: true, |
|
url: 'https://jbilcke-hf-media-server.hf.space/live/comedy.flv', |
|
resolution: '1024x576_24FPS', |
|
model: 'zeroscope_v2_XL', |
|
modelUrl: 'https://huggingface.co/cerspense/zeroscope_v2_XL', |
|
}, |
|
documentary: { |
|
id: 'documentary', |
|
label: 'Documentary', |
|
audience: 0, |
|
online: false, |
|
visible: true, |
|
url: 'https://jbilcke-hf-media-server.hf.space/live/documentary.flv', |
|
resolution: '1024x576_24FPS', |
|
model: 'zeroscope_v2_XL', |
|
modelUrl: 'https://huggingface.co/cerspense/zeroscope_v2_XL', |
|
}, |
|
}, |
|
showToolbar: true, |
|
muted: true, |
|
initialized: false, |
|
activityTimeout: null, |
|
defaultChannelId: 'random', |
|
video: null, |
|
channel: { |
|
}, |
|
wakeUp() { |
|
this.showToolbar = true |
|
clearTimeout(this.activityTimeout) |
|
this.activityTimeout = setTimeout(() => { |
|
this.showToolbar = false |
|
}, 1500); |
|
}, |
|
toggleAudio() { |
|
if (this.video.muted) { |
|
this.video.muted = false |
|
this.muted = false |
|
} else { |
|
this.video.muted = true |
|
this.muted = true |
|
} |
|
}, |
|
async checkAudience() { |
|
let audience = {} |
|
try { |
|
const res = await fetch('/stats') |
|
audience = await res.json() |
|
} catch (err) { |
|
console.log('failed to check the audience, something is wrong') |
|
} |
|
|
|
window.DEBUGME = Object.entries(this.channels) |
|
this.channels = Object.entries(this.channels).reduce((acc, [channel, data]) => ((console.log('debug:', { |
|
...data, |
|
audience: audience[channel] || 0 |
|
} ), { |
|
...acc, |
|
[channel]: { |
|
...data, |
|
audience: audience[channel] || 0 |
|
} |
|
})), {}) |
|
this.channel = this.channels[this.channel.id] |
|
}, |
|
fullscreen() { |
|
if (this.video.requestFullscreen) { |
|
this.video.requestFullscreen(); |
|
} else if (this.video.mozRequestFullScreen) { |
|
this.video.mozRequestFullScreen(); |
|
} else if (this.video.webkitRequestFullscreen) { |
|
this.video.webkitRequestFullscreen(); |
|
} else if (this.video.msRequestFullscreen) { |
|
this.video.msRequestFullscreen(); |
|
} |
|
}, |
|
init() { |
|
if (this.initialized) { |
|
console.log("already initialized") |
|
return |
|
} |
|
this.initialized = true |
|
console.log('AI-TV initialization') |
|
|
|
const urlParams = new URLSearchParams(window.location.search) |
|
|
|
const requestedChannelId = `${urlParams.get('channel') || 'random'}` |
|
|
|
this.enabled = true |
|
|
|
|
|
if (!this.enabled) { |
|
return |
|
} |
|
|
|
this.video = document.getElementById('videoElement') |
|
|
|
const defaultChannel = this.channels[this.defaultChannelId] |
|
|
|
this.channel = this.channels[requestedChannelId] || defaultChannel |
|
|
|
console.log(`Selected channel: ${this.channel.label}`) |
|
console.log(`Stream URL: ${this.channel.url}`) |
|
|
|
|
|
const handleActivity = () => { |
|
this.wakeUp() |
|
} |
|
handleActivity() |
|
|
|
document.addEventListener("touchstart", handleActivity) |
|
document.addEventListener("touchmove", handleActivity) |
|
document.addEventListener("click", handleActivity) |
|
document.addEventListener("mousemove", handleActivity) |
|
|
|
this.checkAudience() |
|
setInterval(() => { |
|
this.checkAudience() |
|
}, 1000) |
|
|
|
|
|
this.video.addEventListener("mute", () => { |
|
this.muted = true |
|
}) |
|
this.video.addEventListener("unmute", () => { |
|
this.muted = false |
|
}) |
|
|
|
|
|
document.addEventListener("mouseleave", () => { |
|
clearTimeout(this.activityTimeout) |
|
this.showToolbar = false |
|
}) |
|
|
|
|
|
this.video.addEventListener('dblclick', () => { |
|
this.fullscreen() |
|
}) |
|
|
|
|
|
if (mpegts.getFeatureList().mseLivePlayback) { |
|
var player = mpegts.createPlayer({ |
|
type: 'flv', |
|
isLive: true, |
|
url: this.channel.url, |
|
}) |
|
player.attachMediaElement(this.video) |
|
|
|
player.on(mpegts.Events.ERROR, function (err) { |
|
console.log('got an error:', err) |
|
if (err.type === mpegts.ErrorTypes.NETWORK_ERROR) { |
|
console.log('Network error') |
|
} |
|
}); |
|
|
|
player.load() |
|
|
|
|
|
|
|
|
|
|
|
this.video.addEventListener('ended', function() { |
|
console.log('Stream ended, trying to reload...') |
|
setTimeout(() => { |
|
console.log('Reloading the page..') |
|
|
|
|
|
|
|
window.location.reload() |
|
}, 1200) |
|
}, false) |
|
|
|
|
|
let promise = this.video.play() |
|
if (promise !== undefined) { |
|
this.video.addEventListener('click', function() { |
|
this.video.play() |
|
}) |
|
} |
|
|
|
player.play() |
|
} |
|
} |
|
} |
|
} |
|
</script> |
|
</body> |
|
</html> |