369 lines
12 KiB
HTML
369 lines
12 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ko">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>🎧 K-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, #42a5f5);
|
|
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-default {
|
|
background: linear-gradient(270deg, #9e9e9e, #bdbdbd);
|
|
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>
|