transcoding: added initial UI for watching webm/hls streams in browser.
Navigation is very primitive. clicking on the logo will step up in channel list, or you can use the up/down arrows on the keyboard. No UI for listing channels is available, as this is more of a test to see what browser are working.
This commit is contained in:
parent
36d27047dd
commit
7ac5542e20
3 changed files with 398 additions and 0 deletions
43
src/webui/static/tv.css
Normal file
43
src/webui/static/tv.css
Normal file
|
@ -0,0 +1,43 @@
|
|||
body {
|
||||
background: black;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
#tv_videoPlayer {
|
||||
z-index: 0;
|
||||
width:100%;
|
||||
margin-right:auto;
|
||||
margin-left:auto;
|
||||
display:block;
|
||||
}
|
||||
|
||||
#tv_channelList {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#tv_statusBar {
|
||||
position: fixed;
|
||||
bottom: 5%;
|
||||
border-radius: 10px;
|
||||
background: rgba(128, 128, 128, 0.7);
|
||||
width: 80%;
|
||||
left: 10%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#tv_statusBar table {
|
||||
padding: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#sb_channelLogo {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
float: left;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#sb_channelName, #sb_nowTitle, #sb_nextTitle {
|
||||
width: 100%;
|
||||
}
|
34
src/webui/static/tv.html
Normal file
34
src/webui/static/tv.html
Normal file
|
@ -0,0 +1,34 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tvheadend</title>
|
||||
<script type="text/javascript" src="extjs/adapter/ext/ext-base.js"></script>
|
||||
<script type="text/javascript" src="extjs/ext-all.js"></script>
|
||||
<script type="text/javascript" src="tv.js"></script>
|
||||
<script type="text/javascript">
|
||||
Ext.onReady(tv.app.init, tv.app);
|
||||
</script>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="tv.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="tv_statusBar">
|
||||
<img id="sb_channelLogo" src="htslogo.png">
|
||||
<table>
|
||||
<tr>
|
||||
<td id="sb_channelName"></td>
|
||||
<td id="sb_currentTime"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="sb_nowTitle"></td>
|
||||
<td id="sb_nowDuration"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td id="sb_nextTitle"></td>
|
||||
<td id="sb_nextDuration"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<video id="tv_videoPlayer"></video>
|
||||
</body>
|
||||
</html>
|
321
src/webui/static/tv.js
Normal file
321
src/webui/static/tv.js
Normal file
|
@ -0,0 +1,321 @@
|
|||
|
||||
if (VK_LEFT === undefined)
|
||||
var VK_LEFT = 0x25;
|
||||
|
||||
if (VK_UP === undefined)
|
||||
var VK_UP = 0x26;
|
||||
|
||||
if (VK_RIGHT === undefined)
|
||||
var VK_RIGHT = 0x27;
|
||||
|
||||
if (VK_DOWN === undefined)
|
||||
var VK_DOWN = 0x28;
|
||||
|
||||
if (VK_ENTER === undefined)
|
||||
var VK_ENTER = 0x0d;
|
||||
|
||||
if (VK_PLAY === undefined)
|
||||
var VK_PLAY = 0x50;
|
||||
|
||||
if (VK_PAUSE === undefined)
|
||||
var VK_PAUSE = 0x51;
|
||||
|
||||
if (VK_STOP === undefined)
|
||||
var VK_STOP = 0x53;
|
||||
|
||||
if (VK_BACK === undefined)
|
||||
var VK_BACK = 0xa6;
|
||||
|
||||
Ext.namespace('tv');
|
||||
|
||||
tv.baseUrl = '../';
|
||||
|
||||
tv.channelTags = new Ext.data.JsonStore({
|
||||
autoLoad : true,
|
||||
root : 'entries',
|
||||
fields : [ 'identifier', 'name' ],
|
||||
id : 'identifier',
|
||||
sortInfo : {
|
||||
field : 'name',
|
||||
direction : "ASC"
|
||||
},
|
||||
url : tv.baseUrl + 'channeltags',
|
||||
baseParams : {
|
||||
op : 'listTags'
|
||||
}
|
||||
});
|
||||
|
||||
tv.channels = new Ext.data.JsonStore({
|
||||
autoLoad : true,
|
||||
root : 'entries',
|
||||
fields : ['ch_icon', 'number', 'name', 'chid', 'tags'],
|
||||
id : 'chid',
|
||||
sortInfo : {
|
||||
field : 'number',
|
||||
direction : "ASC"
|
||||
},
|
||||
url : tv.baseUrl + "channels",
|
||||
baseParams : {
|
||||
op : 'list'
|
||||
}
|
||||
});
|
||||
|
||||
tv.epg = new Ext.data.JsonStore({
|
||||
url : tv.baseUrl + "epg",
|
||||
bufferSize : 300,
|
||||
root : 'entries',
|
||||
fields : [ 'id' ,
|
||||
'channel',
|
||||
'channelid',
|
||||
'title',
|
||||
'subtitle',
|
||||
'episode',
|
||||
'description',
|
||||
'chicon',
|
||||
{
|
||||
name : 'start',
|
||||
type : 'date',
|
||||
dateFormat : 'U' // unix time
|
||||
},
|
||||
{
|
||||
name : 'end',
|
||||
type : 'date',
|
||||
dateFormat : 'U' // unix time
|
||||
},
|
||||
'duration',
|
||||
'contenttype',
|
||||
'schedstate',
|
||||
'serieslink',
|
||||
],
|
||||
baseParams : {
|
||||
limit : 2
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
tv.ui = function() {
|
||||
//private space
|
||||
|
||||
// public space
|
||||
return {
|
||||
};
|
||||
|
||||
}(); // end of ui
|
||||
|
||||
|
||||
|
||||
tv.playback = function() {
|
||||
|
||||
//private space
|
||||
var profiles = [
|
||||
{
|
||||
name: 'pass',
|
||||
muxer: 'pass',
|
||||
audio: 'UNKNOWN',
|
||||
video: 'UNKNOWN',
|
||||
subs: 'UNKNOWN',
|
||||
canPlay: 'video/MP2T'
|
||||
},
|
||||
{
|
||||
name: 'webm',
|
||||
muxer: 'webm',
|
||||
audio: 'VORBIS',
|
||||
video: 'VP8',
|
||||
subs: 'NONE',
|
||||
canPlay: 'video/webm; codecs="vp8.0, vorbis"'
|
||||
},
|
||||
{
|
||||
name: 'hls',
|
||||
muxer: 'mpegts',
|
||||
audio: 'AAC',
|
||||
video: 'H264',
|
||||
subs: 'NONE',
|
||||
canPlay: 'application/vnd.apple.mpegURL; codecs="avc1.42E01E, mp4a.40.2'
|
||||
},
|
||||
{
|
||||
name: 'hls',
|
||||
muxer: 'mpegts',
|
||||
audio: 'AAC',
|
||||
video: 'H264',
|
||||
subs: 'NONE',
|
||||
canPlay: 'application/x-mpegURL; codecs="avc1.42E01E, mp4a.40.2'
|
||||
},
|
||||
{
|
||||
name: 'ts',
|
||||
muxer: 'mpegts',
|
||||
audio: 'AAC',
|
||||
video: 'H264',
|
||||
subs: 'NONE',
|
||||
canPlay: 'video/MP2T; codecs="avc1.42E01E, mp4a.40.2'
|
||||
},
|
||||
{
|
||||
name: 'mkv',
|
||||
muxer: 'matroska',
|
||||
audio: 'AAC',
|
||||
video: 'H264',
|
||||
subs: 'NONE',
|
||||
canPlay: 'video/x-matroska; codecs="avc1.42E01E, mp4a.40.2'
|
||||
}
|
||||
];
|
||||
|
||||
// public space
|
||||
return {
|
||||
getProfile: function() {
|
||||
var vid = document.createElement('video');
|
||||
for(var i=0; i<profiles.length; i++)
|
||||
if(vid.canPlayType(profiles[i].canPlay) == 'probably')
|
||||
return profiles[i];
|
||||
|
||||
for(var i=0; i<profiles.length; i++)
|
||||
if(vid.canPlayType(profiles[i].canPlay) == 'maybe')
|
||||
return profiles[i];
|
||||
|
||||
return null;
|
||||
},
|
||||
|
||||
getUrl: function(chid) {
|
||||
var profile = this.getProfile();
|
||||
var url = tv.baseUrl;
|
||||
|
||||
if(!profile) {
|
||||
alert('Unsupported browser');
|
||||
return '';
|
||||
}
|
||||
|
||||
if(profile.name == 'hls')
|
||||
url += 'playlist/channelid/'
|
||||
else
|
||||
url += 'stream/channelid/'
|
||||
|
||||
console.dir(profile);
|
||||
|
||||
url += chid;
|
||||
url += "?transcode=1";
|
||||
url += "&mux=" + profile.muxer;
|
||||
url += "&acodec=" + profile.audio;
|
||||
url += "&vcodec=" + profile.video;
|
||||
url += "&scodec=" + profile.subs;
|
||||
url += "&resolution=384";
|
||||
|
||||
return url;
|
||||
}
|
||||
};
|
||||
|
||||
}(); // end of player
|
||||
|
||||
tv.app = function() {
|
||||
//private space
|
||||
|
||||
var updateClock = new Ext.util.DelayedTask(function() {
|
||||
var clock = Ext.get('sb_currentTime');
|
||||
clock.update(new Date().toLocaleTimeString());
|
||||
updateClock.delay(1000);
|
||||
});
|
||||
|
||||
var currentChannel = -1;
|
||||
var initKeyboard = function() {
|
||||
|
||||
var channelLogoEl = document.getElementById('sb_channelLogo');
|
||||
channelLogoEl.onclick = function() {
|
||||
currentChannel += 1;
|
||||
ch = tv.channels.getAt(currentChannel);
|
||||
onChannelZap(ch.data);
|
||||
}
|
||||
|
||||
document.onkeyup = function(e) {
|
||||
switch(e.keyCode) {
|
||||
|
||||
case VK_UP:
|
||||
var max = tv.channels.getTotalCount() - 1;
|
||||
if(max < 0)
|
||||
return;
|
||||
|
||||
else if(currentChannel < max)
|
||||
currentChannel += 1;
|
||||
|
||||
else
|
||||
currentChannel = max;
|
||||
|
||||
ch = tv.channels.getAt(currentChannel);
|
||||
onChannelZap(ch.data);
|
||||
break;
|
||||
|
||||
case VK_DOWN:
|
||||
if(tv.channels.getTotalCount() == 0)
|
||||
return;
|
||||
|
||||
else if(currentChannel < 1)
|
||||
currentChannel = 0;
|
||||
|
||||
else
|
||||
currentChannel -= 1;
|
||||
|
||||
ch = tv.channels.getAt(currentChannel);
|
||||
onChannelZap(ch.data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
var onChannelZap = function(ch) {
|
||||
var channelNameEl = document.getElementById('sb_channelName');
|
||||
channelNameEl.innerHTML = ch.name;
|
||||
|
||||
var channelLogoEl = document.getElementById('sb_channelLogo');
|
||||
if(ch.ch_icon) {
|
||||
channelLogoEl.src = ch.ch_icon;
|
||||
channelLogoEl.style = '';
|
||||
} else {
|
||||
channelLogoEl.src = '';
|
||||
channelLogoEl.style = 'display: none';
|
||||
}
|
||||
|
||||
tv.epg.setBaseParam('channel', new String(ch.name));
|
||||
tv.epg.load();
|
||||
|
||||
var videoPlayer = document.getElementById('tv_videoPlayer');
|
||||
|
||||
setTimeout(function() {
|
||||
videoPlayer.src = tv.playback.getUrl(ch.chid);
|
||||
videoPlayer.load();
|
||||
videoPlayer.play();
|
||||
}, 1);
|
||||
}
|
||||
|
||||
var initDataStores = function() {
|
||||
tv.epg.on('load', function() {
|
||||
var nowTitle = document.getElementById('sb_nowTitle');
|
||||
var nowDuration = document.getElementById('sb_nowDuration');
|
||||
var nextTitle = document.getElementById('sb_nextTitle');
|
||||
var nextDuration = document.getElementById('sb_nextDuration');
|
||||
var event = undefined;
|
||||
|
||||
if(tv.epg.getTotalCount() < 1) {
|
||||
nowTitle.innerHTML = '';
|
||||
nowDuration.innerHTML = '';
|
||||
} else {
|
||||
event = tv.epg.getAt(0).data;
|
||||
nowTitle.innerHTML = event.title;
|
||||
nowDuration.innerHTML = event.start.toLocaleTimeString();
|
||||
}
|
||||
|
||||
if(tv.epg.getTotalCount() < 2) {
|
||||
nextTitle.innerHTML = '';
|
||||
nextDuration.innerHTML = '';
|
||||
} else {
|
||||
event = tv.epg.getAt(1).data;
|
||||
nextTitle.innerHTML = event.title;
|
||||
nextDuration.innerHTML = event.start.toLocaleTimeString();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// public space
|
||||
return {
|
||||
init: function() {
|
||||
initDataStores();
|
||||
initKeyboard();
|
||||
updateClock.delay(1);
|
||||
}
|
||||
};
|
||||
}(); // end of app
|
Loading…
Add table
Reference in a new issue