mkRadioStream/radio_stream/index.html

465 lines
15 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<title>🎧 MK-Radio Player</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" />
<link href="https://unpkg.com/video.js/dist/video-js.min.css" rel="stylesheet" />
<style>
:root {
--header-height: 80px;
}
html, body {
height: 100%;
margin: 0;
overflow: hidden;
}
header {
position: fixed;
top: 0;
left: 0;
height: var(--header-height);
width: 100%;
background-color: #222;
z-index: 999;
display: flex;
align-items: center;
padding: 10px 20px;
color: white;
gap: 10px;
}
#audio-player {
width: 250px;
height: 30px;
}
.scroll-title {
font-weight: bold;
white-space: nowrap;
overflow: hidden;
flex-grow: 1;
}
main {
margin-top: var(--header-height); /* 또는 margin-top */
max-height: calc(100vh - var(--header-height));
overflow-y: auto;
}
.vjs-tech {
display: none !important;
}
.vjs-control-bar {
background-color: rgba(255,255,255,0.15);
color: white !important;
}
.card {
transition: transform 0.3s ease;
cursor: pointer;
}
.card:hover {
transform: scale(1.05);
z-index: 1;
box-shadow: 0 8px 16px rgba(0,0,0,0.3);
}
.card-img-top {
max-height: 80px;
object-fit: contain;
padding: 10px;
}
.text-purple { color: #6f42c1 !important; }
.border-purple { border-color: #6f42c1 !important; }
.bg-purple-subtle { background-color: #f3e8ff !important; }
.card-gradient-kbs {
background: linear-gradient(270deg, #1e88e5, #231C99);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-mbc {
background: linear-gradient(270deg, #8e24aa, #ba68c8);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-sbs {
background: linear-gradient(270deg, #43a047, #fdd835);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-ebs {
background: linear-gradient(270deg, #29b6f6, #4dd0e1);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-obs {
background: linear-gradient(270deg, #26247B, #666666);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-ifm {
background: linear-gradient(270deg, #FFB575, #00AEEE);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-ytn {
background: linear-gradient(270deg, #1D9DE9, #231D1F);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-tbs {
background: linear-gradient(270deg, #02BBCE, #F0F0F0);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-cbs {
background: linear-gradient(270deg, #00529B, #E41A35);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-febc {
background: linear-gradient(270deg, #221E1F, #D20C45);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-bbs {
background: linear-gradient(270deg, #D29807, #3D3331);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-cpbc {
background: linear-gradient(270deg, #004D9E, #415066);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-wbs {
background: linear-gradient(270deg, #898989, #666666);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-kookbang {
background: linear-gradient(270deg, #898989, #666666);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-kugak {
background: linear-gradient(270deg, #898989, #666666);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-afn {
background: linear-gradient(270deg, #898989, #666666);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
.card-gradient-default {
background: linear-gradient(270deg, #898989, #666666);
background-size: 400% 400%;
animation: gradientFlow 6s ease infinite;
color: white;
opacity: 0.6; /* 🔧 투명도 살짝 낮춰서 자연스러운 느낌 */
}
@keyframes gradientFlow {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.equalizer {
display: flex;
justify-content: center;
gap: 4px;
margin-top: 8px;
}
.equalizer span {
width: 4px;
height: 20px;
background: #ffc107;
opacity: 0.8;
animation: equalize 1s ease-in-out infinite;
animation-play-state: paused;
}
.equalizer.playing span {
animation-play-state: running;
}
.equalizer span:nth-child(1) { animation-delay: 0s; }
.equalizer span:nth-child(2) { animation-delay: 0.2s; }
.equalizer span:nth-child(3) { animation-delay: 0.4s; }
.equalizer span:nth-child(4) { animation-delay: 0.2s; }
.equalizer span:nth-child(5) { animation-delay: 0s; }
@keyframes equalize {
0%, 100% { transform: scaleY(0.3); }
50% { transform: scaleY(1); }
}
</style>
</head>
<body>
<header>
<button id="_radio_play_stop" class="btn btn-sm btn-outline-light">
<i id="_radio_play_stop_icon" class="bi bi-play-circle-fill"></i>
</button>
<div id="_radio_play_title" class="scroll-title">재생 중인 방송 없음</div>
<audio id="audio-player" class="video-js vjs-default-skin" controls preload="auto"></audio>
</header>
<main class="container-fluid py-3">
<div class="row" id="stream_decks"></div>
</main>
<script src="https://code.jquery.com/jquery-3.6.4.min.js"></script>
<script src="https://unpkg.com/video.js/dist/video.min.js"></script>
<script>
$(document).ready(function () {
const $text = $('#_radio_play_title');
const radio_url = "https://radio.bsod.kr/stream/?";
const audioPlayer = videojs('audio-player', {
controls: true,
autoplay: false,
preload: 'auto',
inactivityTimeout: 0
});
const scrollSpeed = 600;
let textContent = $text.text();
let scrollInterval;
let audioStatus = false;
function startScroll() {
if (!scrollInterval && textContent.length > 0) {
scrollInterval = setInterval(() => {
const chars = Array.from(textContent);
const firstChar = chars.shift();
chars.push(firstChar);
textContent = chars.join('');
$text.text(textContent);
}, scrollSpeed);
}
}
function stopScroll() {
if (scrollInterval) {
clearInterval(scrollInterval);
scrollInterval = null;
}
}
$("#_radio_play_stop").click(() => {
audioStatus = !audioStatus;
audioPlayer[audioStatus ? 'play' : 'pause']();
$("#_radio_play_stop_icon").attr(
'class',
audioStatus ? 'bi-stop-circle-fill' : 'bi-play-circle-fill'
);
audioStatus ? startScroll() : stopScroll();
});
if (!localStorage.getItem("radioRecent")) {
localStorage.setItem("radioRecent", JSON.stringify({
title: "테스트 방송",
query: "stn=kbs&ch=1radio",
timestamp: Date.now()
}));
}
$(document).on("click", ".radio-sta", function () {
const value = $(this).data('value');
const title = value.split(',')[0];
const query = value.split(',')[1];
const streamURL = radio_url + $.trim(query);
const scrollText = "🎵 " + title + " ";
$text.text(scrollText);
textContent = scrollText;
// 저장
localStorage.setItem("radioRecent", JSON.stringify({ title, query, timestamp: Date.now() }));
renderRecentCard(); // 추가
audioPlayer.src({ src: streamURL, type: 'application/x-mpegURL' });
audioPlayer.ready(function () {
audioPlayer.play();
audioStatus = true;
$("#_radio_play_stop_icon").attr('class', 'bi-stop-circle-fill');
startScroll();
});
});
function renderRecentCard() {
const recentRaw = localStorage.getItem("radioRecent");
if (!recentRaw) return;
const recent = JSON.parse(recentRaw);
// 이전 카드 제거
$('#stream_decks .recent-card').remove();
// 새 카드 삽입
$('#stream_decks').prepend(`
<div class="row recent-card">
<div class="col-6">
<div class="card radio-sta border border-warning mb-3" data-value="${recent.title},${recent.query}">
<div class="card-body text-center">
<div class="fw-bold text-warning">최근 재생 방송</div>
<div class="fs-5">${recent.title}</div>
<small class="text-muted">${new Date(recent.timestamp).toLocaleString()}</small>
</div>
</div>
</div>
<div class="col-6">
<div class="card radio-sta border border-warning mb-3">
<div class="card-body text-center">
<div class="fw-bold text-dark">재생 중</div>
<div class="equalizer playing mt-2">
<span></span><span></span><span></span><span></span><span></span>
</div>
<small class="text-dark current-time">00:00:00</small>
</div>
</div>
</div>
</div>
`);
setInterval(() => {
$('.current-time').text(new Date().toLocaleTimeString());
}, 1000);
}
const radioChannels = [
//{ title: "KBS 1라디오", query: "stn=kbs&ch=1radio", img: "images/kbs_radio_logo.png" },
//{ title: "MBC FM4U", query: "stn=mbc&ch=fm4u", img: "" }
{ title: "KBS 1", query: "stn=kbs&ch=1radio", img: "" },
{ title: "KBS 2", query: "stn=kbs&ch=2radio", img: "" },
{ title: "KBS 3", query: "stn=kbs&ch=3radio", img: "" },
{ title: "KBS 1FM", query: "stn=kbs&ch=1fm", img: "" },
{ title: "KBS 2FM", query: "stn=kbs&ch=2fm", img: "" },
{ title: "KBS 한민족", query: "stn=kbs&ch=hanminjok", img: "" },
{ title: "MBC 표준FM", query: "stn=mbc&ch=sfm", img: "" },
{ title: "MBC FM4U", query: "stn=mbc&ch=fm4u", img: "" },
{ title: "MBC 올댓뮤직", query: "stn=mbc&ch=chm", img: "" },
{ title: "SBS 러브FM", query: "stn=sbs&ch=lovefm", img: "" },
{ title: "SBS 파워FM", query: "stn=sbs&ch=powerfm", img: "" },
{ title: "SBS 고릴라M", query: "stn=sbs&ch=dmb", img: "" },
{ title: "EBS FM", query: "stn=ebs", img: "" },
{ title: "OBS라디오", query: "stn=obs", img: "" },
{ title: "iFM 경인방송", query: "stn=ifm", img: "" },
{ title: "YTN라디오", query: "stn=ytn", img: "" },
{ title: "TBS FM", query: "stn=tbs&ch=fm", img: "" },
{ title: "TBSeFM", query: "stn=tbs&ch=efm", img: "" },
{ title: "CBS 표준FM", query: "stn=cbs&ch=sfm", img: "" },
{ title: "CBS 음악FM", query: "stn=cbs&ch=mfm", img: "" },
{ title: "CBS JOY4U", query: "stn=cbs&ch=joy4u", img: "" },
{ title: "FEBC 서울극동", query: "stn=febc", img: "" },
{ title: "BBS 서울불교", query: "stn=bbs", img: "" },
{ title: "CPBC 가톨릭", query: "stn=cpbc", img: "" },
{ title: "WBS 서울음원", query: "stn=wbs", img: "" },
{ title: "국방FM", query: "stn=kookbang", img: "" },
{ title: "국악방송", query: "stn=kugak", img: "" },
{ title: "AFN FM", query: "stn=afn&city=humphreys", img: "" },
];
const brandColors = {
kbs: "primary", // 파란색
mbc: "purple", // 시안
sbs: "success", // 녹색
ebs: "warning", // 노랑
obs: "secondary", // 회색
ifm: "danger", // 붉은색
ytn: "dark",
tbs: "primary",
cbs: "danger",
febc: "secondary",
bbs: "warning",
cpbc: "info",
wbs: "success",
kookbang: "dark",
kugak: "warning",
afn: "light"
};
function renderCards() {
const defaultLogo = "images/radio_default_logo.png"; // 예시: 라디오 아이콘
radioChannels.forEach(channel => {
const stnMatch = channel.query?.match(/stn=([^&]+)/);
const stn = stnMatch ? stnMatch[1] : 'default';
const gradientClass = `card-gradient-${stn}`;
const textColor = (stn === 'default') ? 'text-dark' : 'text-white';
$('#stream_decks').append(`
<div class="col-6 col-sm-4 col-md-3 col-lg-2">
<div class="card radio-sta mb-3 ${gradientClass}" data-value="${channel.title},${channel.query}">
<div class="card-body text-center">
<strong class="${textColor}">${channel.title}</strong>
<small class="d-block mt-1 ${stn === 'default' ? 'text-dark' : 'text-light'}">${stn.toUpperCase()}</small>
</div>
</div>
</div>
`);
});
}
renderRecentCard();
renderCards();
});
</script>
</body>
</html>