Skip to content

Instantly share code, notes, and snippets.

@yyya-nico
Last active May 31, 2024 18:48
Show Gist options
  • Save yyya-nico/f6e6170437599192a5ea2f12eea6aebc to your computer and use it in GitHub Desktop.
Save yyya-nico/f6e6170437599192a5ea2f12eea6aebc to your computer and use it in GitHub Desktop.
KonomiTVですべての地上波チャンネルを同時視聴する

KonomiTVですべての地上波チャンネルを同時視聴する

KonomiTVにファイルを足すことで、地上波多チャンネル同時視聴ができるかもしれない。
当然チューナーは必要数用意
まずフォルダーを以下の場所に作る。

  • {KonomiTVインストールフォルダー}/client/dist/wholech/
  • {KonomiTVインストールフォルダー}/client/dist/wholech/css/
  • {KonomiTVインストールフォルダー}/client/dist/wholech/js/

htmlファイルはwholechフォルダー、cssファイルはcssフォルダー、jsファイルはjsフォルダーに置く。

KonomiTVのバージョンによっては、一部ファイルを書き換える必要もある。

  • {KonomiTVインストールフォルダー}/client/dist/sw.js denylistあたり -> denylist:[/^\/api/,/^\/wholech/]と書き換え

https://{KonomiTVのホスト}/wholech/ にアクセスすることで、見られる。

document.addEventListener('DOMContentLoaded', () => {
const target = document.documentElement;
const btn = document.getElementById("fsbutton");
btn.addEventListener('click', () => {
if (!document.fullscreenElement) {
target.requestFullscreen()
.catch((err) => {
alert('ご利用のブラウザは全画面表示に対応していません' + err.name);
});
} else {
document.exitFullscreen();
}
});
const fullscreenChangeHandler = () => {
if(document.fullscreenElement) {
screen.orientation.lock('landscape').catch(()=>{});
btn.innerHTML = '<i class="material-icons">fullscreen_exit</i>';
btn.title = '全画面表示を終了';
}else{
screen.orientation.unlock && screen.orientation.unlock();
btn.innerHTML = '<i class="material-icons">fullscreen</i>';
btn.title = '全画面表示';
}
}
document.addEventListener('fullscreenchange', fullscreenChangeHandler);
});
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>まるごとch on KonomiTV</title>
<link rel="stylesheet" href="css/style.css">
<script src="js/mpegts.js"></script>
<script src="js/script.js"></script>
<script src="js/fsswitch.js"></script>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<div id="wrap">
<div id="chlist">
</div>
<div id="control">
<a href="/">KonomiTV</a>
<button type="button" id="volumebutton" title="全ch聴く"><i class="material-icons">volume_up</i></button>
<div class="togglesw" title="常に表示">
<input type="checkbox" id="showsw">
<label for="showsw"><i class="material-icons">info</i></label>
</div>
<button type="button" id="fsbutton" title="全画面表示"><i class="material-icons">fullscreen</i></button>
</div>
</div>
</body>
</html>

mpegts.jsも利用するので、最新版をダウンロードして設置してください。
mpegts.js

document.addEventListener('DOMContentLoaded', async () => {
const wrap = document.getElementById('wrap');
const chList = document.getElementById('chlist');
const chFrames = chList.children;
const broadcastInfos = document.getElementsByClassName('broadcast-wrap');
const broadcastTitles = document.getElementsByClassName('broadcast-title');
const controlWraps = document.getElementsByClassName('control');
const controlWrap = document.getElementById('control');
const volumeButton = document.getElementById('volumebutton');
const showSw = document.getElementById('showsw');
const fullscreenButton = document.getElementById('fsbutton');
const controlInit = () => {
let fadeTimer = 0,delayfadeTimer = 0,expandTimer = Array(broadcastTitles.length).fill(0);
function fade() {
clearTimeout(fadeTimer);
clearTimeout(delayfadeTimer);
wrap.classList.remove('hide');
[...broadcastInfos].forEach(broadcastInfo => {
broadcastInfo.classList.remove('hide');
});
[...controlWraps].forEach(controlWrap => {
controlWrap.classList.remove('hide');
});
controlWrap.classList.remove('hide');
fadeTimer = setTimeout(function() {
wrap.classList.add('hide');
[...controlWraps].forEach(controlWrap => {
controlWrap.classList.add('hide');
});
controlWrap.classList.add('hide');
if (showSw.checked) {
return;
}
delayfadeTimer = setTimeout(function() {
[...broadcastInfos].forEach(broadcastInfo => {
broadcastInfo.classList.add('hide');
});
},3000);
},3000);
}
let expandedIndex = null;
function expand(index) {
if (index != expandedIndex) {
if(expandedIndex != null){
broadcastTitles[expandedIndex].classList.remove('expand');
}
expandedIndex = index;
}
clearTimeout(expandTimer[index]);
broadcastTitles[index].classList.add('expand');
expandTimer[index] = setTimeout(function() {
broadcastTitles[index].classList.remove('expand');
},6000);
}
window.addEventListener('pointerdown',fade);
window.addEventListener('pointermove',fade);
window.addEventListener('scroll',fade);
showSw.addEventListener('change',fade);
[...chFrames].forEach((chFrame, index) => {
chFrame.addEventListener('touchstart', () => expand(index));
chFrame.addEventListener('pointermove', () => expand(index));
chFrame.addEventListener('mouseleave',function() {
broadcastTitles[index].classList.remove('expand');
});
});
window.addEventListener('keydown',function(e){//キーボード操作
const keyName = e.key;
function chPick() {
tuning(0);
}
fade();
switch(keyName){
case 'A':
case 'a':
volumeButton.click();
fade();
break;
case 'F':
case 'f':
fullscreenButton.click();
fade();
break;
case 'ArrowUp':
if (!isNaN(listening) && listening !== null) {
if (listening - 3 >= 0) {
tuning(listening - 3);
}
} else {
chPick();
}
expand(listening);
break;
case 'ArrowLeft':
if (!isNaN(listening) && listening !== null) {
if (listening - 1 >= 0) {
tuning(listening - 1);
}
} else {
chPick();
}
expand(listening);
break;
case 'ArrowRight':
if (!isNaN(listening) && listening !== null) {
if (listening + 1 < chFrames.length) {
tuning(listening + 1);
}
} else {
chPick();
}
expand(listening);
break;
case 'ArrowDown':
if (!isNaN(listening) && listening !== null) {
if (listening + 3 < chFrames.length) {
tuning(listening + 3);
}
} else {
chPick();
}
expand(listening);
break;
}
});
volumeButton.addEventListener('click',function() {
listening !== 'all' ? tuning('all') : tuning(null);
});
window.addEventListener('scroll',function() {
function getScrollBottom() {
const body = window.document.body;
const html = window.document.documentElement;
const scrollTop = body.scrollTop || html.scrollTop;
return html.scrollHeight - window.innerHeight - scrollTop;
}
if (getScrollBottom() <= 10) {
//スクロールの位置が下10pxの範囲に来た場合
controlWrap.classList.add('slide');
} else {
//それ以外のスクロールの位置の場合
controlWrap.classList.remove('slide');
}
});
fade();
};
const channelsUpdate = async () => {
channelsList = await fetch('/api/channels')
.then(response => {
if (response.status !== 200) {
console.log('error or no content', response.status);
}
return response.json();
}).catch(e => {
console.error('Failed to load', e);
return null;
});
};
const getGR = () => channelsList.GR;
const getDisplayGR = () => getGR().filter(channel => channel.is_display === true);
let channelsList;
await channelsUpdate();
const getFormattedTime = str => new Date(str).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const tuning = ch => {
switch (ch) {
case null:
chList.classList.remove('choiced');
[...chFrames].forEach(chFrame => {
chFrame.classList.remove('listening');
const video = chFrame.querySelector('video');
video.muted = true;
});
volumeButton.innerHTML = '<i class="material-icons">volume_off</i>';
volumeButton.title = '全ch聴く';
break;
case 'all':
chList.classList.remove('choiced');
[...chFrames].forEach(chFrame => {
chFrame.classList.add('listening');
const video = chFrame.querySelector('video');
video.muted = false;
});
volumeButton.innerHTML = '<i class="material-icons">volume_up</i>';
volumeButton.title = 'ミュートする';
break;
default:
chList.classList.add('choiced');
[...chFrames].forEach((chFrame, index) => {
const video = chFrame.querySelector('video');
if (index === ch) {
chFrame.classList.add('listening');
video.muted = false;
} else {
chFrame.classList.remove('listening');
video.muted = true;
}
});
volumeButton.innerHTML = '<i class="material-icons">volume_up</i>';
volumeButton.title = '全ch聴く';
break;
}
listening = ch;
};
let listening = null;
let p = Promise.resolve();
getDisplayGR().forEach((ch, index) => {
const html =
`<div class="chframe">
<video playsinline controlsList="noremoteplayback"></video>
<div class="broadcast-wrap">
<div class="broadcast-channel-box">
<span class="broadcast-channel">${ch.remocon_id}</span>
<img class="broadcast-logo" src="/api/channels/${ch.id}/logo" alt="${ch.name}">
</div>
<div class="broadcast-title">
<span class="broadcast-title-id">${ch.program_present.title}</span>
<div class="broadcast-time">
<span class="broadcast-start">${getFormattedTime(ch.program_present.start_time)}</span>
<span class="broadcast-to">~</span>
<span class="broadcast-end">${getFormattedTime(ch.program_present.end_time)}</span>
</div>
</div>
</div>
<div class="control">
<button type="button" class="reload" title="再読み込み"><i class="material-icons">refresh</i></button>
</div>
</div>
`;
chList.insertAdjacentHTML('beforeend', html);
const chFrame = chList.lastElementChild;
const video = chFrame.querySelector('video');
const reloadButton = chFrame.querySelector('.reload');
chFrame.addEventListener('click',function() {
if (listening === index) {
tuning(null);
} else {
tuning(index);
}
});
p = p.then(() => {
return new Promise(resolve => {
if (mpegts.getFeatureList().mseLivePlayback) {
const streamPath = `/api/streams/live/${ch.display_channel_id}/360p/mpegts`;
const player = mpegts.createPlayer({
type: 'mse', // could also be mpegts, m2ts, flv
isLive: true,
url: streamPath
});
player.attachMediaElement(video);
reloadButton.addEventListener('click', e => {
e.stopPropagation();
player.unload();
player.load();
player.play();
});
setTimeout(resolve, 500);
player.load();
player.play()
.then(function () {
tuning('all');
})
.catch(function () {
player.muted = true;
player.play();
});
}
});
});
});
controlInit();
await p;
setInterval(async () => {
await channelsUpdate();
getDisplayGR().forEach((ch, index) => {
const broadcastTitle = broadcastTitles[index];
const programMetaElems = {
title: broadcastTitle.querySelector('.broadcast-title-id'),
startTime: broadcastTitle.querySelector('.broadcast-start'),
endTime: broadcastTitle.querySelector('.broadcast-end'),
};
const diffFrom = Object.values(programMetaElems).map(elem => elem.textContent);
const diffTo = [
ch.program_present.title,
getFormattedTime(ch.program_present.start_time),
getFormattedTime(ch.program_present.end_time)
];
const changed = diffFrom.toString() !== diffTo.toString();
if (changed) {
const html =
`<span class="broadcast-title-id">${ch.program_present.title}</span>
<div class="broadcast-time">
<span class="broadcast-start">${getFormattedTime(ch.program_present.start_time)}</span>
<span class="broadcast-to">~</span>
<span class="broadcast-end">${getFormattedTime(ch.program_present.end_time)}</span>
</div>
</div>`;
broadcastTitle.innerHTML = html;
}
});
}, 30 * 1000);
});
*, ::before, ::after{
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
margin: 0;
padding: 0;
color: #EFEFEF;
background: #000;
}
.material-icons {
vertical-align: middle;
}
#wrap {
height: 100%;
display: flex;
justify-content: center;
align-items: center;
}
#wrap.hide, #wrap.hide * {
cursor: none;
}
#chlist {
width: 177.78vh;
max-width: 100%;
height: 56.25vw;
max-height: 100%;
display: grid;
grid-template-rows: repeat(3, 1fr);
grid-template-columns: repeat(3, 1fr);
}
#chlist.choiced {
grid-template-rows: repeat(4, 1fr);
grid-template-columns: repeat(4, 1fr);
}
#chlist > .chframe {
position: relative;
transition: 0.5s;
cursor: pointer;
}
#chlist.choiced > .chframe.listening {
grid-column: 1 / 4;
grid-row: 1 / 4;
}
.chframe video {
width: 100%;
height: 100%;
display: block;
}
.chframe .broadcast-wrap {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
font-size: medium;
visibility: visible;
opacity: 1;
transition: .3s;
}
.chframe .broadcast-wrap.hide {
visibility: hidden;
opacity: 0;
}
.chframe .broadcast-channel-box {
position: absolute;
top: 0;
left: 0;
text-shadow: 0px 0px 1px #000, 1px 1px 2px #000;
}
.chframe .broadcast-channel {
display: inline-block;
width: 25px;
background: rgba(0, 0, 0, 0.5);
border: 2px solid rgba(0, 0, 0, 0.2);
text-align: center;
font-size: 80%;
}
.chframe .broadcast-logo {
width: 1.778em;
height: 1em;
vertical-align: middle;
object-fit: cover;
}
.chframe .broadcast-mute {
color: #ff9;
}
.chframe .broadcast-title {
position: absolute;
left: 0;
right: 0;
bottom: 0;
line-height: 1.2;
height: 1.2em;
background: rgba(0, 0, 0, 0.5);
transition: 0.5s;
overflow: hidden;
}
.chframe .broadcast-title.expand {
height: calc(1.2em * 3);
}
.chframe .broadcast-time {
position: absolute;
top: 3em;
right: 0;
font-size: small;
}
.chframe .control, #control {
right: 0;
transition: .3s;
user-select: none;
}
.chframe .control {
position: absolute;
bottom: 1.2em;
visibility: hidden;
opacity: 0;
}
.chframe:hover .control {
bottom: calc(1.2em * 3);
visibility: visible;
opacity: 1;
}
#control {
position: fixed;
bottom: 0;
visibility: visible;
opacity: 1;
}
.chframe .control.hide, #control.hide {
visibility: hidden;
opacity: 0;
}
.chframe .control > *, #control > * {
display: inline-block;
}
.chframe .control button,
#control a, #control button, #control .togglesw {
min-width: 40px;
height: 40px;
background: transparent;
color: #EFEFEF;
border: none;
vertical-align: middle;
cursor: pointer;
opacity: 0.8;
}
#control > a {
text-decoration: none;
line-height: 40px;
}
.togglesw {
position: relative;
}
.togglesw input[type="checkbox"] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.togglesw label {
width: 100%;
height: 100%;
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 40px;
transition: 0.2s;
}
#control .togglesw :checked + label {
color: #2d96ff;
}
.chframe .control > *:hover, #control > *:hover {
opacity: 1;
}
@media (orientation: portrait),screen and (max-width: 480px) {
html,body,#wrap{
height: auto;
}
#chlist, #chlist.choiced {
height: auto;
max-height: none;
grid-template-columns: 1fr;
grid-template-rows: auto;
}
#chlist.choiced > .chframe.listening {
grid-column: auto;
grid-row: auto;
}
#control {
margin: 10px;
background: rgba(64, 64, 64, 0.8);
border-radius: 3px;
}
#control.slide{
transform: translateY(-60px);
}
#control > * {
display: inline-block;
}
#control a, #control button, #control label {
padding: 1px 6px;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment