Merge branch 'dev'

This commit is contained in:
Luke Pulverenti 2016-05-02 01:49:16 -04:00
commit 63260ab4b0
369 changed files with 54852 additions and 4875 deletions

View File

@ -16,15 +16,14 @@
}, },
"devDependencies": {}, "devDependencies": {},
"ignore": [], "ignore": [],
"version": "1.1.51", "version": "1.1.54",
"_release": "1.1.51", "_release": "1.1.54",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "1.1.51", "tag": "1.1.54",
"commit": "b118029cf5077fc940b3a651054357cbd4f7d04e" "commit": "80b29ca19f79c58f1b9e7f105c5b637761a9c5f9"
}, },
"_source": "https://github.com/MediaBrowser/Emby.ApiClient.Javascript.git", "_source": "https://github.com/MediaBrowser/Emby.ApiClient.Javascript.git",
"_target": "^1.1.51", "_target": "^1.1.51",
"_originalSource": "emby-apiclient", "_originalSource": "emby-apiclient"
"_direct": true
} }

View File

@ -141,7 +141,7 @@
{ {
url: url, url: url,
status: response.status, status: response.status,
errorCode: response.headers ? response.headers["X-Application-Error-Code"] : null errorCode: response.headers ? response.headers.get('X-Application-Error-Code') : null
}]); }]);
} }

View File

@ -215,7 +215,7 @@
return connectUser; return connectUser;
}; };
var minServerVersion = '3.0.5781'; var minServerVersion = '3.0.5785';
self.minServerVersion = function (val) { self.minServerVersion = function (val) {
if (val) { if (val) {

View File

@ -0,0 +1,47 @@
<script>
require(['imageFetcher'], function (imageFetcher) {
window.ImageFetcherLazyloadImage = imageFetcher;
});
/**
* <lazyload-image>
* HTMLImageElement extension for lazy loading.
* http://github.com/1000ch/lazyload-image
*
* Copyright 1000ch
* licensed under the MIT license.
*/
window.LazyloadImage = (function () {
'use strict';
var FALLBACK_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAEElEQVR42gEFAPr/AP///wAI/AL+Sr4t6gAAAABJRU5ErkJggg==';
// create prototype from HTMLImageElement
var LazyloadImagePrototype = Object.create(HTMLImageElement.prototype);
// lifecycle callbacks
//LazyloadImagePrototype.createdCallback = function () {
//};
//LazyloadImagePrototype.attachedCallback = function () {
// var original = this.currentSrc || this.src;
// if (original && window.ImageFetcherLazyloadImage) {
// //console.log(original);
// this.src = FALLBACK_IMAGE;
// console.log('loading ' + original);
// ImageFetcherLazyloadImage.loadImage(this, original);
// }
//};
return document.registerElement('lazyload-image', {
prototype: LazyloadImagePrototype,
extends: 'img'
});
})();
</script>

View File

@ -16,12 +16,12 @@
}, },
"devDependencies": {}, "devDependencies": {},
"ignore": [], "ignore": [],
"version": "1.2.26", "version": "1.2.58",
"_release": "1.2.26", "_release": "1.2.58",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "1.2.26", "tag": "1.2.58",
"commit": "7e13c8d9c17a4946681b3485c5fbf3e62f39cd2f" "commit": "523cb074208c7350bb68554c6fcde613142117f4"
}, },
"_source": "https://github.com/MediaBrowser/emby-webcomponents.git", "_source": "https://github.com/MediaBrowser/emby-webcomponents.git",
"_target": "^1.2.0", "_target": "^1.2.0",

View File

@ -25,12 +25,17 @@
transition: transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1); transition: transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);
transition: transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1); transition: transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);
z-index: -1;*/ z-index: -1;*/
max-height: 84%;
} }
.actionSheet.centered .actionSheetContent { .layout-tv .actionSheet {
text-align: center; max-height: none;
align-items: center; }
}
.actionSheet.centered .actionSheetContent {
text-align: center;
align-items: center;
}
.actionSheetContent { .actionSheetContent {
margin: 0 !important; margin: 0 !important;

View File

@ -15,11 +15,13 @@ define(['dialogHelper', 'layoutManager', 'dialogText', 'html!./../prompt/icons.h
var backButton = false; var backButton = false;
var raisedButtons = false; var raisedButtons = false;
var isFullscreen = false;
if (layoutManager.tv) { if (layoutManager.tv) {
dialogOptions.size = 'fullscreen'; dialogOptions.size = 'fullscreen';
backButton = true; backButton = true;
raisedButtons = true; raisedButtons = true;
isFullscreen = true;
} else { } else {
dialogOptions.modal = false; dialogOptions.modal = false;
@ -42,6 +44,9 @@ define(['dialogHelper', 'layoutManager', 'dialogText', 'html!./../prompt/icons.h
html += '<h2>'; html += '<h2>';
html += options.title; html += options.title;
html += '</h2>'; html += '</h2>';
} else if (!isFullscreen) {
// Add a little space so it's not hugging the border
html += '<br/>';
} }
var text = options.html || options.text; var text = options.html || options.text;
@ -62,7 +67,7 @@ define(['dialogHelper', 'layoutManager', 'dialogText', 'html!./../prompt/icons.h
if (raisedButtons) { if (raisedButtons) {
html += '<paper-button raised class="btnSubmit"><iron-icon icon="dialog:check"></iron-icon><span>' + dialogText.get(buttonText) + '</span></paper-button>'; html += '<paper-button raised class="btnSubmit"><iron-icon icon="dialog:check"></iron-icon><span>' + dialogText.get(buttonText) + '</span></paper-button>';
} else { } else {
html += '<div style="text-align:right;">'; html += '<div class="buttons" style="text-align:right;">';
html += '<paper-button class="btnSubmit">' + dialogText.get(buttonText) + '</paper-button>'; html += '<paper-button class="btnSubmit">' + dialogText.get(buttonText) + '</paper-button>';
html += '</div>'; html += '</div>';
} }

View File

@ -77,11 +77,18 @@ define(['browser'], function (browser) {
// Unfortunately there's no real way to detect mkv support // Unfortunately there's no real way to detect mkv support
if (browser.chrome) { if (browser.chrome) {
var userAgent = navigator.userAgent.toLowerCase();
// Not supported on opera tv // Not supported on opera tv
if (browser.operaTv) { if (browser.operaTv) {
return false; return false;
} }
// Filter out browsers based on chromium that don't support mkv
if (userAgent.indexOf('vivaldi') != -1 || userAgent.indexOf('opera') != -1) {
return false;
}
return true; return true;
} }
@ -318,9 +325,7 @@ define(['browser'], function (browser) {
AudioCodec: hlsVideoAudioCodecs.join(','), AudioCodec: hlsVideoAudioCodecs.join(','),
VideoCodec: 'h264', VideoCodec: 'h264',
Context: 'Streaming', Context: 'Streaming',
Protocol: 'hls', Protocol: 'hls'
// Can't use this when autoplay is not supported
ForceLiveStream: options.supportsCustomSeeking ? true : false
}); });
} }
@ -411,6 +416,12 @@ define(['browser'], function (browser) {
] ]
}); });
var maxLevel = '41';
if (browser.chrome && !browser.mobile) {
maxLevel = '51';
}
profile.CodecProfiles.push({ profile.CodecProfiles.push({
Type: 'Video', Type: 'Video',
Codec: 'h264', Codec: 'h264',
@ -429,7 +440,7 @@ define(['browser'], function (browser) {
{ {
Condition: 'LessThanEqual', Condition: 'LessThanEqual',
Property: 'VideoLevel', Property: 'VideoLevel',
Value: '41' Value: maxLevel
}] }]
}); });

View File

@ -0,0 +1,12 @@
.clearButton {
background: transparent;
border: 0 !important;
padding: 0 !important;
cursor: pointer;
outline: none !important;
color: inherit;
width: 100%;
vertical-align: middle;
font-family: inherit;
font-size: inherit;
}

View File

@ -1,4 +1,4 @@
define([], function () { define(['globalize'], function (globalize) {
function parseISO8601Date(s, toLocal) { function parseISO8601Date(s, toLocal) {
@ -94,11 +94,28 @@ define([], function () {
return parts.join(':'); return parts.join(':');
} }
var toLocaleTimeStringSupportsLocales = function toLocaleTimeStringSupportsLocales() {
try {
new Date().toLocaleTimeString('i');
} catch (e) {
return e.name === 'RangeError';
}
return false;
}();
function getDisplayTime(date) { function getDisplayTime(date) {
var time = date.toLocaleTimeString().toLowerCase();
if (time.indexOf('am') != -1 || time.indexOf('pm') != -1) { var currentLocale = globalize.getCurrentLocale();
var time = currentLocale && toLocaleTimeStringSupportsLocales ?
date.toLocaleTimeString(currentLocale) :
date.toLocaleTimeString();
var timeLower = time.toLowerCase();
if (timeLower.indexOf('am') != -1 || timeLower.indexOf('pm') != -1) {
time = timeLower;
var hour = date.getHours() % 12; var hour = date.getHours() % 12;
var suffix = date.getHours() > 11 ? 'pm' : 'am'; var suffix = date.getHours() > 11 ? 'pm' : 'am';
if (!hour) { if (!hour) {

View File

@ -127,6 +127,10 @@
transition: opacity ease-out 0.2s; transition: opacity ease-out 0.2s;
} }
.mouseIdle .dialog::backdrop {
cursor: none !important;
}
.dialog.opened::backdrop { .dialog.opened::backdrop {
opacity: .6; opacity: .6;
} }

View File

@ -24,8 +24,6 @@
function onBackCommand(e) { function onBackCommand(e) {
if (e.detail.command == 'back') { if (e.detail.command == 'back') {
inputManager.off(dlg, onBackCommand);
self.closedByBack = true; self.closedByBack = true;
closeDialog(dlg); closeDialog(dlg);
e.preventDefault(); e.preventDefault();
@ -34,6 +32,9 @@
function onDialogClosed() { function onDialogClosed() {
inputManager.off(dlg, onBackCommand);
window.removeEventListener('popstate', onHashChange);
removeBackdrop(dlg); removeBackdrop(dlg);
dlg.classList.remove('opened'); dlg.classList.remove('opened');
@ -41,9 +42,6 @@
document.body.classList.remove('noScroll'); document.body.classList.remove('noScroll');
} }
window.removeEventListener('popstate', onHashChange);
inputManager.off(dlg, onBackCommand);
if (!self.closedByBack && isHistoryEnabled(dlg)) { if (!self.closedByBack && isHistoryEnabled(dlg)) {
var state = history.state || {}; var state = history.state || {};
if (state.dialogId == hash) { if (state.dialogId == hash) {

View File

@ -0,0 +1,11 @@
define(['multi-download'], function (multiDownload) {
return {
download: function (items) {
multiDownload(items.map(function (item) {
return item.url;
}));
}
};
});

View File

@ -0,0 +1,408 @@
.tvguide {
display: flex;
flex-direction: column;
align-items: initial;
}
.tvGuideHeader {
white-space: nowrap;
width: 100%;
flex-shrink: 0;
}
.tvProgramSectionHeader {
margin: 0;
}
.tvProgram {
display: block;
text-decoration: none;
white-space: nowrap;
position: relative;
}
.tvProgramTimeSlotInner {
padding: .5em;
}
.tvProgramInfo {
vertical-align: middle;
padding: .5em .5em;
border-bottom: .65vh solid #121212;
}
.tvProgramCurrentTimeSlot {
background-color: green;
}
.tvProgramName {
color: #fff;
margin-bottom: .5em;
}
.tvProgramTime {
color: #fff;
}
.newTvProgram {
color: yellow;
text-transform: uppercase;
}
.liveTvProgram {
color: #64A239;
text-transform: uppercase;
}
.premiereTvProgram {
color: orange;
text-transform: uppercase;
}
.programAccent {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2px;
}
.sportsProgramInfo .programAccent {
background-color: #0A7C33;
}
.newsProgramInfo .programAccent {
background-color: #523378;
}
.movieProgramInfo .programAccent {
background-color: #A43913;
}
.childProgramInfo .programAccent {
background-color: #0B487D;
}
.timerCircle {
display: inline-block;
width: 1em;
height: 1em;
border-radius: 50%;
background: #cc0000;
border: 1px solid #cc3333;
margin-left: 1em;
}
.seriesTimerCircle {
position: relative;
margin-left: 0;
left: 21px;
opacity: .3;
}
.itemMiscInfo .seriesTimerCircle:first-child {
margin-left: 1em;
}
.seriesTimerCircle + .seriesTimerCircle {
opacity: .65;
left: 0;
}
.seriesTimerCircle + .seriesTimerCircle + .seriesTimerCircle {
opacity: 1;
left: -21px;
}
.channelTimeslotHeader {
float: left;
}
.timeslotHeaders {
white-space: nowrap;
}
.mobileGuide .timeslotHeaders {
overflow-x: hidden;
}
.programContainer {
white-space: nowrap;
position: relative;
}
.programGridContainer {
margin-left: 12.2vw;
}
.channelPrograms {
white-space: nowrap;
position: relative;
}
.channelPrograms, .timeslotHeadersInner {
width: 1800vw;
}
@media all and (min-width: 600px) {
.channelPrograms, .timeslotHeadersInner {
width: 1400vw;
}
}
@media all and (min-width: 800px) {
.channelPrograms, .timeslotHeadersInner {
width: 1200vw;
}
}
@media all and (min-width: 1280px) {
.channelPrograms, .timeslotHeadersInner {
width: 660vw;
}
}
.timeslotHeader {
display: inline-flex;
align-items: center;
text-indent: .35em;
}
.channelHeaderCell, .channelTimeslotHeader {
overflow: hidden;
text-overflow: ellipsis;
border-right: 1px solid #121212;
width: 24vw;
background: rgba(40, 40, 40, .9);
display: flex;
align-items: center;
color: #fff !important;
text-decoration: none;
/* Needed in firefox */
text-align: left;
}
@media all and (min-width: 500px) {
.channelHeaderCell, .channelTimeslotHeader {
width: 16vw;
}
}
@media all and (min-width: 600px) {
.channelHeaderCell, .channelTimeslotHeader {
width: 16vw;
}
}
@media all and (min-width: 800px) {
.channelHeaderCell, .channelTimeslotHeader {
width: 14vw;
}
}
@media all and (min-width: 1280px) {
.channelHeaderCell, .channelTimeslotHeader {
width: 12vw;
}
}
.btnSelectDate {
color: #fff;
padding-left: .5em;
}
.channelHeaderCell {
border-bottom: .65vh solid #121212 !important;
background-size: auto 65.7%;
background-position: 90% center;
background-repeat: no-repeat;
}
@media all and (max-width: 800px) {
.guideCurrentDay {
display: none;
}
}
@media all and (max-width: 1280px) {
.channelHeaderCell.withImage .guideChannelNumber {
display: none;
}
}
.channelPrograms, .channelHeaderCell {
height: 15vh;
}
@media all and (min-height: 500px) {
.channelPrograms, .channelHeaderCell {
height: 10.5vh;
}
}
@media all and (min-height: 600px) {
.channelPrograms, .channelHeaderCell {
height: 9vh;
}
}
@media all and (min-height: 720px) {
.channelPrograms, .channelHeaderCell {
height: 6vh;
}
.layout-tv .channelPrograms, .layout-tv .channelHeaderCell {
height: 7.6vh;
}
}
.channelTimeslotHeader {
border-right-color: transparent;
}
.channelTimeslotHeader, .timeslotHeader {
background: transparent;
}
.timeslotHeader, .channelTimeslotHeader {
height: 14vh;
}
@media all and (min-height: 500px) {
.timeslotHeader, .channelTimeslotHeader {
height: 10vh;
}
}
@media all and (min-height: 600px) {
.timeslotHeader, .channelTimeslotHeader {
height: 8vh;
}
}
@media all and (min-height: 720px) {
.timeslotHeader, .channelTimeslotHeader {
height: 5.75vh;
}
}
.pointerInput .channelHeaderCell:hover {
background-color: #444;
}
.channelList {
float: left;
}
.programGrid {
padding-bottom: 4px;
}
.timeslotHeader {
width: 2.0833333333333333333333333333333%;
}
.programCell {
position: absolute;
top: 0;
/* Unfortunately the borders using vh get rounded while the bottom property doesn't. So this is a little hack to try and make them even*/
bottom: .59vh;
border-left: .65vh solid #121212 !important;
background-color: rgba(32, 32, 32, .95);
display: flex;
color: #fff !important;
text-decoration: none;
overflow: hidden;
align-items: center;
/* Needed for Firefox */
text-align: left;
}
.timeslotCellInner {
position: absolute;
bottom: 0;
overflow: hidden;
width: 100%;
top: 0;
display: block;
text-decoration: none;
color: #fff !important;
}
.guideProgramName {
padding: 0 .5em 0;
overflow: hidden;
text-overflow: ellipsis;
}
.guideProgramTime {
padding: 0 .5em .35em;
color: #bbb;
}
.programCell iron-icon {
margin-left: auto;
margin-right: .5em;
height: 3.5vh;
width: 3.5vh;
color: #ddd;
flex-shrink: 0;
}
.programCell iron-icon + iron-icon {
margin-left: .25em;
}
.guideChannelNumber {
padding-left: 1em;
max-width: 30%;
text-overflow: ellipsis;
overflow: hidden;
}
.guideChannelName {
margin-left: auto;
margin-right: 1em;
max-width: 40%;
text-overflow: ellipsis;
overflow: hidden;
}
@media all and (max-width: 1000px) {
.guideChannelName {
display: none;
}
}
.channelList, .programGrid {
height: auto !important;
}
.programCell:focus, .channelHeaderCell:focus, .btnSelectDate:focus {
background-color: #52B54B;
outline: none !important;
}
.programCell:focus .programAccent {
background-color: transparent !important;
}
.timerIcon, .seriesTimerIcon {
color: #cc3333 !important;
}

View File

@ -0,0 +1,648 @@
define(['globalize', 'connectionManager', 'loading', 'scrollHelper', 'datetime', 'focusManager', 'imageLoader', 'events', 'layoutManager', 'itemShortcuts', 'registrationservices', 'clearButtonStyle', 'css!./guide.css', 'html!./icons.html', 'scrollStyles'], function (globalize, connectionManager, loading, scrollHelper, datetime, focusManager, imageLoader, events, layoutManager, itemShortcuts, registrationServices) {
var baseUrl;
function Guide(options) {
var self = this;
var items = {};
self.refresh = function () {
reloadPage(options.element);
};
self.destroy = function () {
itemShortcuts.off(options.element);
items = {};
};
self.options = options;
// 30 mins
var cellCurationMinutes = 30;
var cellDurationMs = cellCurationMinutes * 60 * 1000;
var msPerDay = 86400000;
var currentDate;
var channelQuery = {
StartIndex: 0,
EnableFavoriteSorting: true
};
var channelsPromise;
function normalizeDateToTimeslot(date) {
var minutesOffset = date.getMinutes() - cellCurationMinutes;
if (minutesOffset >= 0) {
date.setHours(date.getHours(), cellCurationMinutes, 0, 0);
} else {
date.setHours(date.getHours(), 0, 0, 0);
}
return date;
}
function showLoading() {
loading.show();
}
function hideLoading() {
loading.hide();
}
function getChannelLimit(context) {
return registrationServices.validateFeature('livetv').then(function () {
var limit = 400;
context.querySelector('.guideRequiresUnlock').classList.add('hide');
return limit;
}, function () {
var limit = 5;
context.querySelector('.guideRequiresUnlock').classList.remove('hide');
context.querySelector('.unlockText').innerHTML = globalize.translate('MessageLiveTvGuideRequiresUnlock', limit);
return limit;
});
}
function reloadGuide(context, newStartDate) {
var apiClient = connectionManager.currentApiClient();
channelQuery.UserId = apiClient.getCurrentUserId();
getChannelLimit(context).then(function (channelLimit) {
showLoading();
channelQuery.Limit = channelLimit;
channelQuery.AddCurrentProgram = false;
channelsPromise = channelsPromise || apiClient.getLiveTvChannels(channelQuery);
var date = newStartDate;
// Add one second to avoid getting programs that are just ending
date = new Date(date.getTime() + 1000);
// Subtract to avoid getting programs that are starting when the grid ends
var nextDay = new Date(date.getTime() + msPerDay - 2000);
console.log(nextDay);
channelsPromise.then(function (channelsResult) {
apiClient.getLiveTvPrograms({
UserId: apiClient.getCurrentUserId(),
MaxStartDate: nextDay.toISOString(),
MinEndDate: date.toISOString(),
channelIds: channelsResult.Items.map(function (c) {
return c.Id;
}).join(','),
ImageTypeLimit: 1,
EnableImageTypes: "Primary,Backdrop",
SortBy: "StartDate"
}).then(function (programsResult) {
renderGuide(context, date, channelsResult.Items, programsResult.Items, apiClient);
hideLoading();
});
});
});
}
function getDisplayTime(date) {
if ((typeof date).toString().toLowerCase() === 'string') {
try {
date = datetime.parseISO8601Date(date, { toLocal: true });
} catch (err) {
return date;
}
}
return datetime.getDisplayTime(date).toLowerCase();
}
function getTimeslotHeadersHtml(startDate, endDateTime) {
var html = '';
// clone
startDate = new Date(startDate.getTime());
html += '<div class="timeslotHeadersInner">';
while (startDate.getTime() < endDateTime) {
html += '<div class="timeslotHeader">';
html += getDisplayTime(startDate);
html += '</div>';
// Add 30 mins
startDate.setTime(startDate.getTime() + cellDurationMs);
}
html += '</div>';
return html;
}
function parseDates(program) {
if (!program.StartDateLocal) {
try {
program.StartDateLocal = datetime.parseISO8601Date(program.StartDate, { toLocal: true });
} catch (err) {
}
}
if (!program.EndDateLocal) {
try {
program.EndDateLocal = datetime.parseISO8601Date(program.EndDate, { toLocal: true });
} catch (err) {
}
}
return null;
}
function getChannelProgramsHtml(context, date, channel, programs) {
var html = '';
var startMs = date.getTime();
var endMs = startMs + msPerDay - 1;
programs = programs.filter(function (curr) {
return curr.ChannelId == channel.Id;
});
html += '<div class="channelPrograms">';
for (var i = 0, length = programs.length; i < length; i++) {
var program = programs[i];
if (program.ChannelId != channel.Id) {
continue;
}
parseDates(program);
if (program.EndDateLocal.getTime() < startMs) {
continue;
}
if (program.StartDateLocal.getTime() > endMs) {
break;
}
items[program.Id] = program;
var renderStartMs = Math.max(program.StartDateLocal.getTime(), startMs);
var startPercent = (program.StartDateLocal.getTime() - startMs) / msPerDay;
startPercent *= 100;
startPercent = Math.max(startPercent, 0);
var renderEndMs = Math.min(program.EndDateLocal.getTime(), endMs);
var endPercent = (renderEndMs - renderStartMs) / msPerDay;
endPercent *= 100;
var cssClass = "programCell clearButton itemAction";
var addAccent = true;
if (program.IsKids) {
cssClass += " childProgramInfo";
} else if (program.IsSports) {
cssClass += " sportsProgramInfo";
} else if (program.IsNews) {
cssClass += " newsProgramInfo";
} else if (program.IsMovie) {
cssClass += " movieProgramInfo";
}
else {
cssClass += " plainProgramInfo";
addAccent = false;
}
html += '<button data-action="link" data-isfolder="' + program.IsFolder + '" data-id="' + program.Id + '" data-serverid="' + program.ServerId + '" data-type="' + program.Type + '" class="' + cssClass + '" style="left:' + startPercent + '%;width:' + endPercent + '%;">';
var guideProgramNameClass = "guideProgramName";
html += '<div class="' + guideProgramNameClass + '">';
if (program.IsLive) {
html += '<span class="liveTvProgram">' + globalize.translate('core#AttributeLive') + '&nbsp;</span>';
}
else if (program.IsPremiere) {
html += '<span class="premiereTvProgram">' + globalize.translate('core#AttributePremiere') + '&nbsp;</span>';
}
else if (program.IsSeries && !program.IsRepeat) {
html += '<span class="newTvProgram">' + globalize.translate('core#AttributeNew') + '&nbsp;</span>';
}
html += program.Name;
html += '</div>';
if (program.IsHD) {
html += '<iron-icon icon="guide:hd"></iron-icon>';
}
if (program.SeriesTimerId) {
html += '<iron-icon class="seriesTimerIcon" icon="guide:fiber-smart-record"></iron-icon>';
}
else if (program.TimerId) {
html += '<iron-icon class="timerIcon" icon="guide:fiber-manual-record"></iron-icon>';
}
if (addAccent) {
html += '<div class="programAccent"></div>';
}
html += '</button>';
}
html += '</div>';
return html;
}
function renderPrograms(context, date, channels, programs) {
var html = [];
for (var i = 0, length = channels.length; i < length; i++) {
html.push(getChannelProgramsHtml(context, date, channels[i], programs));
}
var programGrid = context.querySelector('.programGrid');
programGrid.innerHTML = html.join('');
programGrid.scrollTop = 0;
programGrid.scrollLeft = 0;
}
function renderChannelHeaders(context, channels, apiClient) {
var html = '';
for (var i = 0, length = channels.length; i < length; i++) {
var channel = channels[i];
var hasChannelImage = channel.ImageTags.Primary;
var dataSrc = '';
if (hasChannelImage) {
var url = apiClient.getScaledImageUrl(channel.Id, {
maxHeight: 200,
tag: channel.ImageTags.Primary,
type: "Primary"
});
dataSrc = ' data-src="' + url + '"';
}
var cssClass = 'channelHeaderCell clearButton itemAction lazy';
if (hasChannelImage) {
cssClass += ' withImage';
}
html += '<button type="button" class="' + cssClass + '"' + dataSrc + ' data-action="link" data-isfolder="' + channel.IsFolder + '" data-id="' + channel.Id + '" data-serverid="' + channel.ServerId + '" data-type="' + channel.Type + '">';
html += '<div class="guideChannelNumber">' + channel.Number + '</div>';
if (!hasChannelImage) {
html += '<div class="guideChannelName">' + channel.Name + '</div>';
}
html += '</button>';
}
var channelList = context.querySelector('.channelList');
channelList.innerHTML = html;
imageLoader.lazyChildren(channelList);
}
function renderGuide(context, date, channels, programs, apiClient) {
//var list = [];
//channels.forEach(function(i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels.forEach(function (i) {
// list.push(i);
//});
//channels = list;
renderChannelHeaders(context, channels, apiClient);
var startDate = date;
var endDate = new Date(startDate.getTime() + msPerDay);
context.querySelector('.timeslotHeaders').innerHTML = getTimeslotHeadersHtml(startDate, endDate);
items = {};
renderPrograms(context, date, channels, programs);
if (layoutManager.tv) {
focusManager.autoFocus(context.querySelector('.programGrid'), true);
}
}
function nativeScrollTo(container, pos, horizontal) {
if (container.scrollTo) {
if (horizontal) {
container.scrollTo(pos, 0);
} else {
container.scrollTo(0, pos);
}
} else {
if (horizontal) {
container.scrollLeft = Math.round(pos);
} else {
container.scrollTop = Math.round(pos);
}
}
}
var lastGridScroll = 0;
var lastHeaderScroll = 0;
function onProgramGridScroll(context, elem, timeslotHeaders) {
if ((new Date().getTime() - lastHeaderScroll) >= 1000) {
lastGridScroll = new Date().getTime();
nativeScrollTo(timeslotHeaders, elem.scrollLeft, true);
}
}
function onTimeslotHeadersScroll(context, elem, programGrid) {
if ((new Date().getTime() - lastGridScroll) >= 1000) {
lastHeaderScroll = new Date().getTime();
nativeScrollTo(programGrid, elem.scrollLeft, true);
}
}
function getFutureDateText(date) {
var weekday = [];
weekday[0] = globalize.translate('core#OptionSundayShort');
weekday[1] = globalize.translate('core#OptionMondayShort');
weekday[2] = globalize.translate('core#OptionTuesdayShort');
weekday[3] = globalize.translate('core#OptionWednesdayShort');
weekday[4] = globalize.translate('core#OptionThursdayShort');
weekday[5] = globalize.translate('core#OptionFridayShort');
weekday[6] = globalize.translate('core#OptionSaturdayShort');
var day = weekday[date.getDay()];
date = date.toLocaleDateString();
if (date.toLowerCase().indexOf(day.toLowerCase()) == -1) {
return day + " " + date;
}
return date;
}
function changeDate(page, date) {
var newStartDate = normalizeDateToTimeslot(date);
currentDate = newStartDate;
reloadGuide(page, newStartDate);
var text = getFutureDateText(date);
text = '<span class="guideCurrentDay">' + text.replace(' ', ' </span>');
page.querySelector('.btnSelectDate').innerHTML = text;
}
var dateOptions = [];
function setDateRange(page, guideInfo) {
var today = new Date();
today.setHours(today.getHours(), 0, 0, 0);
var start = datetime.parseISO8601Date(guideInfo.StartDate, { toLocal: true });
var end = datetime.parseISO8601Date(guideInfo.EndDate, { toLocal: true });
start.setHours(0, 0, 0, 0);
end.setHours(0, 0, 0, 0);
if (start.getTime() >= end.getTime()) {
end.setDate(start.getDate() + 1);
}
start = new Date(Math.max(today, start));
dateOptions = [];
while (start <= end) {
dateOptions.push({
name: getFutureDateText(start),
id: start.getTime()
});
start.setDate(start.getDate() + 1);
start.setHours(0, 0, 0, 0);
}
var date = new Date();
if (currentDate) {
date.setTime(currentDate.getTime());
}
changeDate(page, date);
}
function reloadPage(page) {
showLoading();
var apiClient = connectionManager.currentApiClient();
apiClient.getLiveTvGuideInfo().then(function (guideInfo) {
setDateRange(page, guideInfo);
});
}
function selectDate(page) {
require(['actionsheet'], function (actionsheet) {
actionsheet.show({
items: dateOptions,
title: globalize.translate('core#HeaderSelectDate'),
callback: function (id) {
var date = new Date();
date.setTime(parseInt(id));
changeDate(page, date);
}
});
});
}
function createVerticalScroller(view, pageInstance) {
if (layoutManager.tv) {
scrollHelper.centerFocus.on(view.querySelector('.smoothScrollY'), false);
var programGrid = view.querySelector('.programGrid');
scrollHelper.centerFocus.on(programGrid, true);
}
}
function parentWithClass(elem, className) {
while (!elem.classList || !elem.classList.contains(className)) {
elem = elem.parentNode;
if (!elem) {
return null;
}
}
return elem;
}
var selectedMediaInfoTimeout;
var focusedElement;
function onProgramGridFocus(e) {
var programCell = parentWithClass(e.target, 'programCell');
if (!programCell) {
return;
}
focusedElement = e.target;
if (selectedMediaInfoTimeout) {
clearTimeout(selectedMediaInfoTimeout);
}
selectedMediaInfoTimeout = setTimeout(onSelectedMediaInfoTimeout, 700);
}
function onSelectedMediaInfoTimeout() {
var focused = focusedElement
if (focused && document.activeElement == focused) {
var id = focused.getAttribute('data-id');
var item = items[id];
if (item) {
events.trigger(self, 'focus', [
{
item: item
}]);
}
}
}
var xhr = new XMLHttpRequest();
xhr.open('GET', baseUrl + '/tvguide.template.html', true);
xhr.onload = function (e) {
var template = this.response;
var context = options.element;
context.innerHTML = globalize.translateDocument(template, 'core');
var programGrid = context.querySelector('.programGrid');
var timeslotHeaders = context.querySelector('.timeslotHeaders');
programGrid.addEventListener('focus', onProgramGridFocus, true);
programGrid.addEventListener('scroll', function () {
onProgramGridScroll(context, this, timeslotHeaders);
});
timeslotHeaders.addEventListener('scroll', function () {
onTimeslotHeadersScroll(context, this, programGrid);
});
context.querySelector('.btnSelectDate').addEventListener('click', function () {
selectDate(context);
});
context.querySelector('.btnUnlockGuide').addEventListener('click', function () {
reloadPage(context);
});
context.classList.add('tvguide');
createVerticalScroller(context, self);
itemShortcuts.on(context);
events.trigger(self, 'load');
self.refresh();
}
xhr.send();
};
Guide.setBaseUrl = function (url) {
baseUrl = url;
};
return Guide;
});

View File

@ -0,0 +1,42 @@
<!--
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->
<!--
`iron-icons` is a utility import that includes the definition for the `iron-icon` element, `iron-iconset-svg` element, as well as an import for the default icon set.
The `iron-icons` directory also includes imports for additional icon sets that can be loaded into your project.
Example loading icon set:
<link rel="import" href="../iron-icons/maps-icons.html">
To use an icon from one of these sets, first prefix your `iron-icon` with the icon set name, followed by a colon, ":", and then the icon id.
Example using the directions-bus icon from the maps icon set:
<iron-icon icon="maps:directions-bus"></iron-icon>
See [iron-icon](#iron-icon) for more information about working with icons.
See [iron-iconset](#iron-iconset) and [iron-iconset-svg](#iron-iconset-svg) for more information about how to create a custom iconset.
@group Iron Elements
@element iron-icons
@demo demo/index.html
-->
<iron-iconset-svg name="guide" size="24">
<svg>
<defs>
<g id="fiber-manual-record"><circle cx="12" cy="12" r="8" /></g>
<g id="fiber-smart-record"><g><circle cx="9" cy="12" r="8" /><path d="M17 4.26v2.09c2.33.82 4 3.04 4 5.65s-1.67 4.83-4 5.65v2.09c3.45-.89 6-4.01 6-7.74s-2.55-6.85-6-7.74z" /></g></g>
<g id="hd"><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-8 12H9.5v-2h-2v2H6V9h1.5v2.5h2V9H11v6zm2-6h4c.55 0 1 .45 1 1v4c0 .55-.45 1-1 1h-4V9zm1.5 4.5h2v-3h-2v3z" /></g>
</defs>
</svg>
</iron-iconset-svg>

View File

@ -0,0 +1,22 @@
<div class="tvGuideHeader">
<div class="channelTimeslotHeader">
<button class="btnSelectDate clearButton" style="display:block;"></button>
</div>
<div class="timeslotHeaders smoothScrollX" style="scroll-behavior: auto;"></div>
</div>
<div class="smoothScrollY guideVerticalScroller" style="flex-grow: 1;">
<div class="programContainer">
<div class="channelList"></div>
<div class="programGridContainer programGrid smoothScrollX" style="white-space: nowrap;">
</div>
</div>
</div>
<div class="guideRequiresUnlock readOnlyContent hide" style="margin:1em auto;text-align:center;padding:1em;flex-shrink:0;">
<p class="unlockText"></p>
<paper-button raised class="secondary block btnUnlockGuide"><iron-icon icon="check"></iron-icon><span>${ButtonUnlockGuide}</span></paper-button>
</div>

View File

@ -4,16 +4,8 @@ define([], function () {
if (elem.tagName !== "IMG") { if (elem.tagName !== "IMG") {
return new Promise(function (resolve, reject) { elem.style.backgroundImage = "url('" + url + "')";
return Promise.resolve(elem);
var tmp = new Image();
tmp.onload = function () {
elem.style.backgroundImage = "url('" + url + "')";
resolve(elem);
};
tmp.src = url;
});
} else { } else {
elem.setAttribute("src", url); elem.setAttribute("src", url);

View File

@ -4,16 +4,8 @@ define(['cryptojs-md5'], function () {
if (elem.tagName !== "IMG") { if (elem.tagName !== "IMG") {
return new Promise(function (resolve, reject) { elem.style.backgroundImage = "url('" + url + "')";
return Promise.resolve(elem);
var tmp = new Image();
tmp.onload = function () {
elem.style.backgroundImage = "url('" + url + "')";
resolve(elem);
};
tmp.src = url;
});
} else { } else {
elem.setAttribute("src", url); elem.setAttribute("src", url);

View File

@ -1,4 +1,4 @@
define(['layoutManager', 'MaterialSpinner', 'css!./loading'], function (layoutManager) { define(['MaterialSpinner', 'css!./loading'], function () {
return { return {
show: function () { show: function () {
@ -11,10 +11,6 @@ define(['layoutManager', 'MaterialSpinner', 'css!./loading'], function (layoutMa
elem.classList.add('mdl-spinner'); elem.classList.add('mdl-spinner');
elem.classList.add('mdl-js-spinner'); elem.classList.add('mdl-js-spinner');
if (layoutManager.tv) {
elem.classList.add('tv');
}
document.body.appendChild(elem); document.body.appendChild(elem);
componentHandler.upgradeElement(elem, 'MaterialSpinner'); componentHandler.upgradeElement(elem, 'MaterialSpinner');
} }

View File

@ -1,21 +1,14 @@
.docspinner { .docspinner {
margin-top: -4vh; margin-top: -5vh;
margin-left: -4vh; margin-left: -5vh;
width: 7vh; width: 10vh;
height: 7vh; height: 10vh;
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
z-index: 9999999; z-index: 9999999;
} }
.docspinner.tv {
margin-top: -5vh;
margin-left: -5vh;
width: 10vh;
height: 10vh;
}
.loadingHide { .loadingHide {
display: none !important; display: none !important;
} }

View File

@ -1,4 +1,4 @@
define(['layoutManager', 'paper-spinner', 'css!./loading'], function (layoutManager) { define(['paper-spinner', 'css!./loading'], function () {
return { return {
show: function () { show: function () {
@ -9,10 +9,6 @@ define(['layoutManager', 'paper-spinner', 'css!./loading'], function (layoutMana
elem = document.createElement("paper-spinner"); elem = document.createElement("paper-spinner");
elem.classList.add('docspinner'); elem.classList.add('docspinner');
if (layoutManager.tv) {
elem.classList.add('tv');
}
document.body.appendChild(elem); document.body.appendChild(elem);
} }

View File

@ -0,0 +1,73 @@
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.multiDownload = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
'use strict';
function fallback(urls) {
var i = 0;
(function createIframe() {
var frame = document.createElement('iframe');
frame.style.display = 'none';
frame.src = urls[i++];
document.documentElement.appendChild(frame);
// the download init has to be sequential otherwise IE only use the first
var interval = setInterval(function () {
if (frame.contentWindow.document.readyState === 'complete') {
clearInterval(interval);
// Safari needs a timeout
setTimeout(function () {
frame.parentNode.removeChild(frame);
}, 1000);
if (i < urls.length) {
createIframe();
}
}
}, 100);
})();
}
function isFirefox() {
// sad panda :(
return /Firefox\//i.test(navigator.userAgent);
}
function sameDomain(url) {
var a = document.createElement('a');
a.href = url;
return location.hostname === a.hostname && location.protocol === a.protocol;
}
function download(url) {
var a = document.createElement('a');
a.download = '';
a.href = url;
// firefox doesn't support `a.click()`...
a.dispatchEvent(new MouseEvent('click'));
}
module.exports = function (urls) {
if (!urls) {
throw new Error('`urls` required');
}
if (typeof document.createElement('a').download === 'undefined') {
return fallback(urls);
}
var delay = 0;
urls.forEach(function (url) {
// the download init has to be sequential for firefox if the urls are not on the same domain
if (isFirefox() && !sameDomain(url)) {
return setTimeout(download.bind(null, url), 100 * ++delay);
}
download(url);
});
}
},{}]},{},[1])(1)
});

View File

@ -12,7 +12,7 @@
.promptDialogContent { .promptDialogContent {
text-align: left; text-align: left;
padding: 2em; padding: 1em 2em;
margin: 0 !important; margin: 0 !important;
} }

View File

@ -271,17 +271,17 @@ define(['loading', 'viewManager', 'skinManager', 'pluginManager', 'backdrop', 'b
var apiClient = connectionManager.currentApiClient(); var apiClient = connectionManager.currentApiClient();
var pathname = ctx.pathname.toLowerCase(); var pathname = ctx.pathname.toLowerCase();
console.log('Emby.Page - processing path request ' + pathname); console.log('embyRouter - processing path request ' + pathname);
if ((!apiClient || !apiClient.isLoggedIn()) && !route.anonymous) { if ((!apiClient || !apiClient.isLoggedIn()) && !route.anonymous) {
console.log('Emby.Page - route does not allow anonymous access, redirecting to login'); console.log('embyRouter - route does not allow anonymous access, redirecting to login');
beginConnectionWizard(); beginConnectionWizard();
return; return;
} }
if (apiClient && apiClient.isLoggedIn()) { if (apiClient && apiClient.isLoggedIn()) {
console.log('Emby.Page - user is authenticated'); console.log('embyRouter - user is authenticated');
var isCurrentRouteStartup = currentRouteInfo ? currentRouteInfo.route.startup : true; var isCurrentRouteStartup = currentRouteInfo ? currentRouteInfo.route.startup : true;
if (ctx.isBack && (route.isDefaultRoute || route.startup) && !isCurrentRouteStartup) { if (ctx.isBack && (route.isDefaultRoute || route.startup) && !isCurrentRouteStartup) {
@ -289,7 +289,7 @@ define(['loading', 'viewManager', 'skinManager', 'pluginManager', 'backdrop', 'b
return; return;
} }
else if (route.isDefaultRoute) { else if (route.isDefaultRoute) {
console.log('Emby.Page - loading skin home page'); console.log('embyRouter - loading skin home page');
skinManager.loadUserSkin(); skinManager.loadUserSkin();
return; return;
} else if (route.roles) { } else if (route.roles) {
@ -303,7 +303,7 @@ define(['loading', 'viewManager', 'skinManager', 'pluginManager', 'backdrop', 'b
} }
} }
console.log('Emby.Page - proceeding to ' + pathname); console.log('embyRouter - proceeding to ' + pathname);
callback(); callback();
} }
@ -484,8 +484,10 @@ define(['loading', 'viewManager', 'skinManager', 'pluginManager', 'backdrop', 'b
function showItem(item) { function showItem(item) {
if (typeof (item) === 'string') { if (typeof (item) === 'string') {
Emby.Models.item(item).then(showItem); require(['connectionManager'], function (connectionManager) {
var apiClient = connectionManager.currentApiClient();
apiClient.getItem(apiClient.getCurrentUserId(), item).then(showItem);
});
} else { } else {
skinManager.getCurrentSkin().showItem(item); skinManager.getCurrentSkin().showItem(item);
} }

View File

@ -1,4 +1,4 @@
.smoothScrollX { .smoothScrollX, .hiddenScrollX {
overflow-x: auto; overflow-x: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
overflow-y: hidden; overflow-y: hidden;
@ -6,45 +6,30 @@
white-space: nowrap; white-space: nowrap;
} }
.hiddenScrollX { .hiddenScrollX, .layout-tv .smoothScrollX {
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
overflow-y: hidden;
white-space: nowrap;
scroll-behavior: smooth;
}
.hiddenScrollX, .layout-tv .smoothScrollX, .layout-mobile .smoothScrollX {
-ms-overflow-style: none; -ms-overflow-style: none;
overflow: -moz-scrollbars-none; overflow: -moz-scrollbars-none;
} }
.hiddenScrollX::-webkit-scrollbar, .layout-tv .smoothScrollX::-webkit-scrollbar, .layout-mobile .smoothScrollX::-webkit-scrollbar { .hiddenScrollX::-webkit-scrollbar, .layout-tv .smoothScrollX::-webkit-scrollbar {
height: 0 !important; height: 0 !important;
display: none; display: none;
} }
.smoothScrollY { .smoothScrollY, .hiddenScrollY {
overflow-y: auto; overflow-y: auto;
-webkit-overflow-scrolling: touch; -webkit-overflow-scrolling: touch;
overflow-x: hidden; overflow-x: hidden;
scroll-behavior: smooth; scroll-behavior: smooth;
} }
.hiddenScrollY, .layout-tv .smoothScrollY, .layout-mobile .smoothScrollY { .hiddenScrollY, .layout-tv .smoothScrollY {
overflow-y: auto;
-webkit-overflow-scrolling: touch;
overflow-x: hidden;
scroll-behavior: smooth;
}
.hiddenScrollY {
-ms-overflow-style: none; -ms-overflow-style: none;
overflow: -moz-scrollbars-none; overflow: -moz-scrollbars-none;
} }
.hiddenScrollY::-webkit-scrollbar, .layout-tv .smoothScrollY::-webkit-scrollbar, .layout-mobile .smoothScrollY::-webkit-scrollbar { .hiddenScrollY::-webkit-scrollbar, .layout-tv .smoothScrollY::-webkit-scrollbar {
width: 0 !important; width: 0 !important;
display: none; display: none;
} }

View File

@ -0,0 +1,60 @@
define(['connectionManager', 'sharingMenu', 'loading'], function (connectionManager, sharingMenu, loading) {
function onSharingCancel(options, apiClient) {
var shareId = options.share.Id;
console.log('share cancelled. shareId: ' + shareId);
// Delete the share since it was cancelled
apiClient.ajax({
type: 'DELETE',
url: apiClient.getUrl('Social/Shares/' + shareId)
});
}
function showMenu(options) {
loading.show();
var itemId = options.itemId;
var apiClient = connectionManager.getApiClient(options.serverId);
var userId = apiClient.getCurrentUserId();
return apiClient.getItem(userId, itemId).then(function () {
return apiClient.ajax({
type: 'POST',
url: apiClient.getUrl('Social/Shares', {
ItemId: itemId,
UserId: userId
}),
dataType: "json"
}).then(function (share) {
var options = {
share: share
};
loading.hide();
sharingMenu.showMenu(options).then(function() {
console.log('share success. shareId: ' + options.share.Id);
}, function () {
onSharingCancel(options, apiClient);
});
}, function () {
loading.hide();
});
});
}
return {
showMenu: showMenu
};
});

View File

@ -1,34 +1,28 @@
define(['dialogHelper', 'jQuery', 'thirdparty/social-share-kit-1.0.4/dist/js/social-share-kit.min', 'css!thirdparty/social-share-kit-1.0.4/dist/css/social-share-kit.css'], function (dialogHelper, $) { define(['dialogHelper', 'layoutManager', 'dialogText', './social-share-kit-1.0.4/dist/js/social-share-kit.min', 'css!./social-share-kit-1.0.4/dist/css/social-share-kit.css'], function (dialogHelper, layoutManager, dialogText) {
function showMenu(options, successCallback, cancelCallback) { function showMenu(options) {
var dlg = dialogHelper.createDialog({ var dlg = dialogHelper.createDialog({
removeOnClose: true, removeOnClose: true,
autoFocus: false autoFocus: layoutManager.tv
}); });
dlg.id = 'dlg' + new Date().getTime(); dlg.id = 'dlg' + new Date().getTime();
var html = ''; var html = '';
html += '<h2>' + Globalize.translate('HeaderShare') + '</h2>'; html += '<h2>' + Globalize.translate('Share') + '</h2>';
html += '<div>'; html += '<div>';
html += '<div class="ssk-group ssk-round ssk-lg">'; html += '<div class="ssk-group ssk-round ssk-lg">';
// We can only do facebook if we can guarantee that the current page is available over the internet, since FB will try to probe it. // We can only do facebook if we can guarantee that the current page is available over the internet, since FB will try to probe it.
if (Dashboard.isConnectMode()) { html += '<a href="" class="ssk ssk-facebook"></a>';
html += '<a href="" class="ssk ssk-facebook"></a>';
}
html += '<a href="" class="ssk ssk-twitter"></a><a href="" class="ssk ssk-google-plus"></a><a href="" class="ssk ssk-pinterest"></a><a href="" class="ssk ssk-tumblr"></a></div>'; html += '<a href="" class="ssk ssk-twitter"></a><a href="" class="ssk ssk-google-plus"></a><a href="" class="ssk ssk-pinterest"></a><a href="" class="ssk ssk-tumblr"></a></div>';
html += '</div>'; html += '</div>';
html += '<div style="max-width:240px;">';
html += Globalize.translate('ButtonShareHelp');
html += '</div>';
html += '<div class="buttons">'; html += '<div class="buttons">';
html += '<paper-button class="btnCancel" dialog-dismiss>' + Globalize.translate('ButtonCancel') + '</paper-button>'; html += '<paper-button class="btnCancel">' + dialogText.get('Cancel') + '</paper-button>';
html += '</div>'; html += '</div>';
dlg.innerHTML = html; dlg.innerHTML = html;
@ -48,28 +42,36 @@
via: 'Emby' via: 'Emby'
}); });
// Has to be assigned a z-index after the call to .open() function onSskButtonClick(e) {
dlg.addEventListener('close', function () {
if (isShared) {
successCallback(options);
} else {
cancelCallback(options);
}
});
// Has to be assigned a z-index after the call to .open()
$('.ssk', dlg).on('click', function () {
isShared = true; isShared = true;
dialogHelper.close(dlg); dialogHelper.close(dlg);
}); }
// Has to be assigned a z-index after the call to .open()
var sskButtons = dlg.querySelectorAll('.ssk');
for (var i = 0, length = sskButtons.length; i < length; i++) {
sskButtons[i].addEventListener('click', onSskButtonClick);
}
// Has to be assigned a z-index after the call to .open() // Has to be assigned a z-index after the call to .open()
dlg.querySelector('.btnCancel').addEventListener('click', function () { dlg.querySelector('.btnCancel').addEventListener('click', function () {
dialogHelper.close(dlg); dialogHelper.close(dlg);
}); });
var promise = new Promise(function (resolve, reject) {
dlg.addEventListener('close', function () {
if (isShared) {
resolve();
} else {
reject();
}
});
});
dialogHelper.open(dlg); dialogHelper.open(dlg);
return promise;
} }
return { return {

View File

@ -0,0 +1,168 @@
define(['playbackManager', 'inputManager', 'connectionManager', 'embyRouter'], function (playbackManager, inputManager, connectionManager, embyRouter) {
function playAllFromHere(card, serverId) {
var cards = card.parentNode.querySelectorAll('.itemAction[data-id]');
var ids = [];
var foundCard = false;
for (var i = 0, length = cards.length; i < length; i++) {
if (cards[i] == card) {
foundCard = true;
}
if (foundCard) {
ids.push(cards[i].getAttribute('data-id'));
}
}
playbackManager.play({
ids: ids,
serverId: serverId
});
}
function showSlideshow(startItemId, serverId) {
var apiClient = connectionManager.getApiClient(serverId);
var userId = apiClient.getCurrentUserId();
return apiClient.getItem(userId, startItemId).then(function (item) {
return apiClient.getItems(userId, {
MediaTypes: 'Photo',
Filters: 'IsNotFolder',
ParentId: item.ParentId
}).then(function (result) {
var items = result.Items;
var index = items.map(function (i) {
return i.Id;
}).indexOf(startItemId);
if (index == -1) {
index = 0;
}
require(['slideshow'], function (slideshow) {
var newSlideShow = new slideshow({
showTitle: false,
cover: false,
items: items,
startIndex: index,
interval: 8000,
interactive: true
});
newSlideShow.show();
});
});
});
}
function showItem(options) {
if (options.Type == 'Photo') {
showSlideshow(options.Id, options.ServerId);
return;
}
embyRouter.showItem(options);
}
function executeAction(card, action) {
var id = card.getAttribute('data-id');
var serverId = card.getAttribute('data-serverid');
var type = card.getAttribute('data-type');
var isfolder = card.getAttribute('data-isfolder') == 'true';
if (action == 'link') {
showItem({
Id: id,
Type: type,
IsFolder: isfolder,
ServerId: serverId
});
}
else if (action == 'instantmix') {
playbackManager.instantMix(id, serverId);
}
else if (action == 'play') {
var startPositionTicks = parseInt(card.getAttribute('data-startpositionticks') || '0');
playbackManager.play({
ids: [id],
startPositionTicks: startPositionTicks,
serverId: serverId
});
}
else if (action == 'playallfromhere') {
playAllFromHere(card, serverId);
}
else if (action == 'setplaylistindex') {
}
}
function onClick(e) {
var card = parentWithClass(e.target, 'itemAction');
if (card) {
var action = card.getAttribute('data-action');
if (action) {
executeAction(card, action);
}
}
}
function parentWithClass(elem, className) {
while (!elem.classList || !elem.classList.contains(className)) {
elem = elem.parentNode;
if (!elem) {
return null;
}
}
return elem;
}
function onCommand(e) {
var cmd = e.detail.command;
if (cmd == 'play') {
var card = parentWithClass(e.target, 'itemAction');
if (card) {
executeAction(card, cmd);
}
}
}
function on(context) {
context.addEventListener('click', onClick);
inputManager.on(context, onCommand);
}
function off(context) {
context.removeEventListener('click', onClick);
inputManager.off(context, onCommand);
}
return {
on: on,
off: off
};
});

View File

@ -34,11 +34,13 @@ See [iron-iconset](#iron-iconset) and [iron-iconset-svg](#iron-iconset-svg) for
<iron-iconset-svg name="slideshow" size="24"> <iron-iconset-svg name="slideshow" size="24">
<svg> <svg>
<defs> <defs>
<g id="arrow-back"><path d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z" /></g> <g id="close"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z" /></g>
<g id="pause"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /></g> <g id="pause"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z" /></g>
<g id="skip-next"><path d="M6 18l8.5-6L6 6v12zM16 6v12h2V6h-2z" /></g> <g id="keyboard-arrow-left"><path d="M15.41 16.09l-4.58-4.59 4.58-4.59L14 5.5l-6 6 6 6z" /></g>
<g id="skip-previous"><path d="M6 6h2v12H6zm3.5 6l8.5 6V6z" /></g> <g id="keyboard-arrow-right"><path d="M8.59 16.34l4.58-4.59-4.58-4.59L10 5.75l6 6-6 6z" /></g>
<g id="play-arrow"><path d="M8 5v14l11-7z" /></g> <g id="play-arrow"><path d="M8 5v14l11-7z" /></g>
<g id="file-download"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" /></g>
<g id="share"><path d="M18 16.08c-.76 0-1.44.3-1.96.77L8.91 12.7c.05-.23.09-.46.09-.7s-.04-.47-.09-.7l7.05-4.11c.54.5 1.25.81 2.04.81 1.66 0 3-1.34 3-3s-1.34-3-3-3-3 1.34-3 3c0 .24.04.47.09.7L8.04 9.81C7.5 9.31 6.79 9 6 9c-1.66 0-3 1.34-3 3s1.34 3 3 3c.79 0 1.5-.31 2.04-.81l7.12 4.16c-.05.21-.08.43-.08.65 0 1.61 1.31 2.92 2.92 2.92 1.61 0 2.92-1.31 2.92-2.92s-1.31-2.92-2.92-2.92z" /></g>
</defs> </defs>
</svg> </svg>
</iron-iconset-svg> </iron-iconset-svg>

View File

@ -1,16 +1,117 @@
define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'css!./style', 'html!./icons', 'iron-icon-set', 'paper-fab', 'paper-icon-button', 'paper-spinner'], function (dialogHelper, inputmanager, connectionManager, layoutManager) { define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'focusManager', 'apphost', 'css!./style', 'html!./icons', 'iron-icon-set', 'paper-icon-button', 'paper-spinner'], function (dialogHelper, inputmanager, connectionManager, layoutManager, focusManager, appHost) {
function getImageUrl(item, options, apiClient) {
options = options || {};
options.type = options.type || "Primary";
if (typeof (item) === 'string') {
return apiClient.getScaledImageUrl(item, options);
}
if (item.ImageTags && item.ImageTags[options.type]) {
options.tag = item.ImageTags[options.type];
return apiClient.getScaledImageUrl(item.Id, options);
}
if (options.type == 'Primary') {
if (item.AlbumId && item.AlbumPrimaryImageTag) {
options.tag = item.AlbumPrimaryImageTag;
return apiClient.getScaledImageUrl(item.AlbumId, options);
}
//else if (item.AlbumId && item.SeriesPrimaryImageTag) {
// imgUrl = ApiClient.getScaledImageUrl(item.SeriesId, {
// type: "Primary",
// width: downloadWidth,
// tag: item.SeriesPrimaryImageTag,
// minScale: minScale
// });
//}
//else if (item.ParentPrimaryImageTag) {
// imgUrl = ApiClient.getImageUrl(item.ParentPrimaryImageItemId, {
// type: "Primary",
// width: downloadWidth,
// tag: item.ParentPrimaryImageTag,
// minScale: minScale
// });
//}
}
return null;
}
function getBackdropImageUrl(item, options, apiClient) {
options = options || {};
options.type = options.type || "Backdrop";
options.width = null;
delete options.width;
options.maxWidth = null;
delete options.maxWidth;
options.maxHeight = null;
delete options.maxHeight;
options.height = null;
delete options.height;
// If not resizing, get the original image
if (!options.maxWidth && !options.width && !options.maxHeight && !options.height) {
options.quality = 100;
}
if (item.BackdropImageTags && item.BackdropImageTags.length) {
options.tag = item.BackdropImageTags[0];
return apiClient.getScaledImageUrl(item.Id, options);
}
return null;
}
function getImgUrl(item, original) {
var apiClient = connectionManager.getApiClient(item.ServerId);
var imageOptions = {};
if (!original) {
imageOptions.maxWidth = screen.availWidth;
}
if (item.BackdropImageTags && item.BackdropImageTags.length) {
return getBackdropImageUrl(item, imageOptions, apiClient);
} else {
if (item.MediaType == 'Photo' && original) {
return apiClient.getUrl("Items/" + item.Id + "/Download", {
api_key: apiClient.accessToken()
});
}
imageOptions.type = "Primary";
return getImageUrl(item, imageOptions, apiClient);
}
}
return function (options) { return function (options) {
var self = this; var self = this;
var swiperInstance; var swiperInstance;
var dlg; var dlg;
var currentTimeout;
var currentIntervalMs;
var currentOptions;
var currentIndex;
function createElements(options) { function createElements(options) {
dlg = dialogHelper.createDialog({ dlg = dialogHelper.createDialog({
exitAnimationDuration: options.interactive ? 400 : 800, exitAnimationDuration: options.interactive ? 400 : 800,
size: 'fullscreen' size: 'fullscreen',
autoFocus: false
}); });
dlg.classList.add('slideshowDialog'); dlg.classList.add('slideshowDialog');
@ -19,16 +120,41 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'c
if (options.interactive) { if (options.interactive) {
var actionButtonsOnTop = layoutManager.mobile;
html += '<div>'; html += '<div>';
html += '<div class="slideshowSwiperContainer"><div class="swiper-wrapper"></div></div>'; html += '<div class="slideshowSwiperContainer"><div class="swiper-wrapper"></div></div>';
html += '<paper-fab mini icon="slideshow:arrow-back" class="btnSlideshowExit" tabindex="-1"></paper-fab>'; html += '<paper-icon-button icon="slideshow:keyboard-arrow-left" class="btnSlideshowPrevious slideshowButton" tabindex="-1"></paper-icon-button>';
html += '<paper-icon-button icon="slideshow:keyboard-arrow-right" class="btnSlideshowNext slideshowButton" tabindex="-1"></paper-icon-button>';
html += '<div class="slideshowControlBar">'; html += '<div class="topActionButtons">';
html += '<paper-icon-button icon="slideshow:skip-previous" class="btnSlideshowPrevious slideshowButton"></paper-icon-button>'; if (actionButtonsOnTop) {
html += '<paper-icon-button icon="slideshow:pause" class="btnSlideshowPause slideshowButton" autoFocus></paper-icon-button>'; if (appHost.supports('filedownload')) {
html += '<paper-icon-button icon="slideshow:skip-next" class="btnSlideshowNext slideshowButton"></paper-icon-button>'; html += '<paper-icon-button icon="slideshow:file-download" class="btnDownload slideshowButton"></paper-icon-button>';
}
if (appHost.supports('sharing')) {
html += '<paper-icon-button icon="slideshow:share" class="btnShare slideshowButton"></paper-icon-button>';
}
}
html += '<paper-icon-button icon="slideshow:close" class="btnSlideshowExit" tabindex="-1"></paper-icon-button>';
html += '</div>'; html += '</div>';
if (!actionButtonsOnTop) {
html += '<div class="slideshowBottomBar hide">';
//html += '<paper-icon-button icon="slideshow:share" class="btnShare slideshowButton"></paper-icon-button>';
html += '<paper-icon-button icon="slideshow:pause" class="btnSlideshowPause slideshowButton" autoFocus></paper-icon-button>';
if (appHost.supports('filedownload')) {
html += '<paper-icon-button icon="slideshow:file-download" class="btnDownload slideshowButton"></paper-icon-button>';
}
if (appHost.supports('sharing')) {
html += '<paper-icon-button icon="slideshow:share" class="btnShare slideshowButton"></paper-icon-button>';
}
html += '</div>';
}
html += '</div>'; html += '</div>';
} else { } else {
@ -44,7 +170,21 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'c
}); });
dlg.querySelector('.btnSlideshowNext').addEventListener('click', nextImage); dlg.querySelector('.btnSlideshowNext').addEventListener('click', nextImage);
dlg.querySelector('.btnSlideshowPrevious').addEventListener('click', previousImage); dlg.querySelector('.btnSlideshowPrevious').addEventListener('click', previousImage);
dlg.querySelector('.btnSlideshowPause').addEventListener('click', playPause);
var btnPause = dlg.querySelector('.btnSlideshowPause');
if (btnPause) {
btnPause.addEventListener('click', playPause);
}
var btnDownload = dlg.querySelector('.btnDownload');
if (btnDownload) {
btnDownload.addEventListener('click', download);
}
var btnShare = dlg.querySelector('.btnShare');
if (btnShare) {
btnShare.addEventListener('click', share);
}
} }
document.body.appendChild(dlg); document.body.appendChild(dlg);
@ -56,6 +196,7 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'c
}); });
inputmanager.on(window, onInputCommand); inputmanager.on(window, onInputCommand);
document.addEventListener('mousemove', onMouseMove);
dlg.addEventListener('close', onDialogClosed); dlg.addEventListener('close', onDialogClosed);
@ -101,9 +242,12 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'c
function getSwiperSlideHtmlFromItem(item) { function getSwiperSlideHtmlFromItem(item) {
return getSwiperSlideHtmlFromSlide({ return getSwiperSlideHtmlFromSlide({
imageUrl: getImgUrl(item) imageUrl: getImgUrl(item),
originalImage: getImgUrl(item, true),
//title: item.Name, //title: item.Name,
//description: item.Overview //description: item.Overview
Id: item.Id,
ServerId: item.ServerId
}); });
} }
@ -128,7 +272,7 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'c
function getSwiperSlideHtmlFromSlide(item) { function getSwiperSlideHtmlFromSlide(item) {
var html = ''; var html = '';
html += '<div class="swiper-slide">'; html += '<div class="swiper-slide" data-original="' + item.originalImage + '" data-itemid="' + item.Id + '" data-serverid="' + item.ServerId + '">';
html += '<img data-src="' + item.imageUrl + '" class="swiper-lazy">'; html += '<img data-src="' + item.imageUrl + '" class="swiper-lazy">';
html += '<paper-spinner></paper-spinner>'; html += '<paper-spinner></paper-spinner>';
if (item.title || item.subtitle) { if (item.title || item.subtitle) {
@ -179,15 +323,59 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'c
} }
} }
function getCurrentImageInfo() {
if (swiperInstance) {
var slide = document.querySelector('.swiper-slide-active');
if (slide) {
return {
url: slide.getAttribute('data-original'),
itemId: slide.getAttribute('data-itemid'),
serverId: slide.getAttribute('data-serverid')
};
}
return null;
} else {
return null;
}
}
function download() {
var imageInfo = getCurrentImageInfo();
require(['fileDownloader'], function (fileDownloader) {
fileDownloader.download([imageInfo]);
});
}
function share() {
var imageInfo = getCurrentImageInfo();
require(['sharingmanager'], function (sharingManager) {
sharingManager.showMenu(imageInfo);
});
}
function play() { function play() {
dlg.querySelector('.btnSlideshowPause').icon = "slideshow:pause"; var btnSlideshowPause = dlg.querySelector('.btnSlideshowPause');
if (btnSlideshowPause) {
btnSlideshowPause.icon = "slideshow:pause";
}
swiperInstance.startAutoplay(); swiperInstance.startAutoplay();
} }
function pause() { function pause() {
dlg.querySelector('.btnSlideshowPause').icon = "slideshow:play-arrow"; var btnSlideshowPause = dlg.querySelector('.btnSlideshowPause');
if (btnSlideshowPause) {
btnSlideshowPause.icon = "slideshow:play-arrow";
}
swiperInstance.stopAutoplay(); swiperInstance.stopAutoplay();
} }
@ -212,13 +400,9 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'c
} }
inputmanager.off(window, onInputCommand); inputmanager.off(window, onInputCommand);
document.removeEventListener('mousemove', onMouseMove);
} }
var currentTimeout;
var currentIntervalMs;
var currentOptions;
var currentIndex;
function startInterval(options) { function startInterval(options) {
currentOptions = options; currentOptions = options;
@ -232,93 +416,143 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'c
} }
} }
function getImgUrl(item) { var _osdOpen = false;
function isOsdOpen() {
return _osdOpen;
}
var apiClient = connectionManager.getApiClient(item.ServerId); function getOsdBottom() {
if (item.BackdropImageTags && item.BackdropImageTags.length) { return dlg.querySelector('.slideshowBottomBar');
return getBackdropImageUrl(item, { }
maxWidth: screen.availWidth
}, apiClient); function showOsd() {
} else {
return getImageUrl(item, { var bottom = getOsdBottom();
type: "Primary", if (bottom) {
maxWidth: screen.availWidth slideUpToShow(bottom);
}, apiClient); startHideTimer();
} }
} }
function getBackdropImageUrl(item, options, apiClient) { function hideOsd() {
options = options || {}; var bottom = getOsdBottom();
options.type = options.type || "Backdrop"; if (bottom) {
slideDownToHide(bottom);
options.width = null;
delete options.width;
options.maxWidth = null;
delete options.maxWidth;
options.maxHeight = null;
delete options.maxHeight;
options.height = null;
delete options.height;
// If not resizing, get the original image
if (!options.maxWidth && !options.width && !options.maxHeight && !options.height) {
options.quality = 100;
} }
if (item.BackdropImageTags && item.BackdropImageTags.length) {
options.tag = item.BackdropImageTags[0];
return apiClient.getScaledImageUrl(item.Id, options);
}
return null;
} }
function getImageUrl(item, options, apiClient) { var hideTimeout;
function startHideTimer() {
stopHideTimer();
hideTimeout = setTimeout(hideOsd, 4000);
}
function stopHideTimer() {
if (hideTimeout) {
clearTimeout(hideTimeout);
hideTimeout = null;
}
}
options = options || {}; function slideUpToShow(elem) {
options.type = options.type || "Primary";
if (typeof (item) === 'string') { if (!elem.classList.contains('hide')) {
return apiClient.getScaledImageUrl(item, options); return;
} }
if (item.ImageTags && item.ImageTags[options.type]) { _osdOpen = true;
elem.classList.remove('hide');
options.tag = item.ImageTags[options.type]; requestAnimationFrame(function () {
return apiClient.getScaledImageUrl(item.Id, options);
var keyframes = [
{ transform: 'translate3d(0,' + elem.offsetHeight + 'px,0)', opacity: '.3', offset: 0 },
{ transform: 'translate3d(0,0,0)', opacity: '1', offset: 1 }];
var timing = { duration: 300, iterations: 1, easing: 'ease-out' };
elem.animate(keyframes, timing).onfinish = function () {
focusManager.focus(elem.querySelector('.btnSlideshowPause'));
};
});
}
function slideDownToHide(elem) {
if (elem.classList.contains('hide')) {
return;
} }
if (options.type == 'Primary') { requestAnimationFrame(function () {
if (item.AlbumId && item.AlbumPrimaryImageTag) {
options.tag = item.AlbumPrimaryImageTag; var keyframes = [
return apiClient.getScaledImageUrl(item.AlbumId, options); { transform: 'translate3d(0,0,0)', opacity: '1', offset: 0 },
} { transform: 'translate3d(0,' + elem.offsetHeight + 'px,0)', opacity: '.3', offset: 1 }];
var timing = { duration: 300, iterations: 1, easing: 'ease-out' };
elem.animate(keyframes, timing).onfinish = function () {
elem.classList.add('hide');
_osdOpen = false;
};
});
}
//else if (item.AlbumId && item.SeriesPrimaryImageTag) { var lastMouseMoveData;
function onMouseMove(e) {
// imgUrl = ApiClient.getScaledImageUrl(item.SeriesId, { var eventX = e.screenX || 0;
// type: "Primary", var eventY = e.screenY || 0;
// width: downloadWidth,
// tag: item.SeriesPrimaryImageTag,
// minScale: minScale
// });
//} var obj = lastMouseMoveData;
//else if (item.ParentPrimaryImageTag) { if (!obj) {
lastMouseMoveData = {
// imgUrl = ApiClient.getImageUrl(item.ParentPrimaryImageItemId, { x: eventX,
// type: "Primary", y: eventY
// width: downloadWidth, };
// tag: item.ParentPrimaryImageTag, return;
// minScale: minScale
// });
//}
} }
return null; // if coord are same, it didn't move
if (Math.abs(eventX - obj.x) < 10 && Math.abs(eventY - obj.y) < 10) {
return;
}
obj.x = eventX;
obj.y = eventY;
showOsd();
}
function onInputCommand(e) {
switch (e.detail.command) {
case 'left':
if (!isOsdOpen()) {
e.preventDefault();
previousImage();
}
break;
case 'right':
if (!isOsdOpen()) {
e.preventDefault();
nextImage();
}
break;
case 'up':
case 'down':
case 'select':
case 'menu':
case 'info':
case 'play':
case 'playpause':
case 'pause':
case 'fastforward':
case 'rewind':
case 'next':
case 'previous':
showOsd();
break;
default:
break;
}
} }
function showNextImage(index, skipPreload) { function showNextImage(index, skipPreload) {
@ -397,33 +631,6 @@ define(['dialogHelper', 'inputManager', 'connectionManager', 'layoutManager', 'c
} }
} }
function onInputCommand(e) {
switch (e.detail.command) {
case 'left':
previousImage();
break;
case 'right':
nextImage();
break;
case 'play':
play();
break;
case 'pause':
pause();
break;
case 'playpause':
playPause();
break;
default:
return
break;
}
e.preventDefault();
}
self.show = function () { self.show = function () {
startInterval(options); startInterval(options);
}; };

View File

@ -8,7 +8,6 @@
right: 0; right: 0;
left: 0; left: 0;
bottom: 0; bottom: 0;
z-index: 1001;
background-position: center center; background-position: center center;
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
@ -46,43 +45,84 @@
top: 50%; top: 50%;
} }
.btnSlideshowExit { .slideshowDialog paper-icon-button {
width: 5.2vh;
height: 5.2vh;
color: #fff;
opacity: .7;
min-width: 40px;
min-height: 40px;
}
.layout-tv .slideshowDialog paper-icon-button {
width: 7vh;
height: 7vh;
}
@media only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3), only screen and (min-resolution: 120dpi) {
.slideshowDialog paper-icon-button {
width: 11vmin;
height: 11vmin;
}
}
.btnSlideshowPrevious {
left: .5vh;
top: 45vh;
z-index: 1002; z-index: 1002;
position: absolute; position: absolute;
top: 1.5vh;
left: 1.5vh;
width: 6vh;
height: 6vh;
color: #eee;
} }
paper-fab.btnSlideshowExit { .btnSlideshowNext {
background-color: #444; right: .5vh;
top: 45vh;
z-index: 1002;
position: absolute;
} }
.slideshowControlBar { .topActionButtons {
right: .5vh;
top: .5vh;
z-index: 1002;
position: absolute;
}
.slideshowBottomBar {
position: fixed; position: fixed;
left: 0; left: 0;
bottom: 0; bottom: 0;
right: 0; right: 0;
z-index: 1002; background-color: rgba(0, 0, 0, .7);
background: rgba(0,0,0,.5); color: #fff;
text-align: center; padding: .5%;
color: #eee; display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
} }
.mouseIdle .btnSlideshowExit { .slideshowTopBar {
position: fixed;
left: 0;
top: 0;
right: 0;
background-color: rgba(0, 0, 0, .7);
color: #fff;
padding: .5%;
display: flex;
flex-direction: row;
align-items: center;
text-align: right;
justify-content: flex-end;
}
.mouseIdle .btnSlideshowPrevious, .mouseIdle .btnSlideshowNext, .mouseIdle .btnSlideshowExit {
display: none; display: none;
} }
.mouseIdle .slideshowControlBar { .slideshowExtraButtons {
transform: translateY(100%); margin-left: auto;
transition: transform 600ms ease-out; text-align: right;
}
.slideshowButton {
width: 8vh;
height: 8vh;
} }
.slideText { .slideText {

View File

@ -0,0 +1,36 @@
{
"name": "fingerprintjs2",
"description": "Modern & flexible browser fingerprinting library",
"main": "dist/fingerprint2.min.js",
"moduleType": [
"es6"
],
"keywords": [
"browser",
"fingerprint",
"fingerprinting",
"security",
"privacy"
],
"authors": [
"Valentin Vasilev"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"spec"
],
"homepage": "https://github.com/Valve/fingerprintjs2",
"version": "1.1.4",
"_release": "1.1.4",
"_resolution": {
"type": "version",
"tag": "1.1.4",
"commit": "ae5719db3d942a1a84ab43b707d4d1c34138934e"
},
"_source": "https://github.com/Valve/fingerprintjs2.git",
"_target": "^1.1.3",
"_originalSource": "fingerprintjs2"
}

View File

@ -0,0 +1,35 @@
Contributing to FingerprintJS2
==============================
## Found a bug?
Please submit an issue.
Include in the issue:
* List of components you received in the `get` call (make sure values are not truncated)
* If FP is different every time you call the library, include 2 versions of components
* Include your OS version
* Include steps to reproduce
* Include library call code (I need all options you used when calling the library function)
## Want to add a feature / contribute?
* Fork the project and make the required changes in it (don't forget to add specs)
* PRs w/out specs will not be accepted
* Run `gulp` to catch stylistic errors and produce the minified version.
* Run specs by opening the `specs/spec_runner.html` or typing `npm test` (requires phantomjs for console running).
* Make a PR.
* Make sure you only make one commit per feature you want to add
* Make sure your commit message is descriptive and tells what you changed (`Updated the library` - that's a bad commit message)
If your code changes the list of fingerprinting sources, please update
the README.
If you're unsure about the feature you want to add, submit an issue with
a `question` tag.
## Want to ask?
* Please read FAQ first
* If you have not found the answer you were looking for - use gitter.im to ask your question (link is in the readme)
Happy Coding!

View File

@ -0,0 +1,18 @@
#### Can I use this library to uniquely identify users?
##### No, you cannot. This library is built to be able to associate string identifiers with devices. Since there are a lot of identical devices, you will get a lot of identical identifiers.
#### OK, I get it, I cannot _uniquely_ identify users, but can I identify users at all?
##### No, you cannot. This library is strictly for non-deterministic device identification.
#### How good is your library? Can you guarantee that different devices will have different identifiers?
##### This library is not good. It has an error margin of 10-20%
#### Can you improve the library to be 100% accurate for device identification?
##### I don't think it is possible now and don't think it will be possible in the future.
#### Can you improve the library to be more accurate (since you cannot make it 100% accurate)?
##### I can, but it takes a lot of time. I need a lot of devices, enviroments and more importantly - time. Since this is my hobby project, I spend very little time on it.
#### How can I build a complete identification solution?
##### You should either use commercial services, such as https://augur.io, or develop such service yourself. If you don't know how to do it, please use StackOverflow.

View File

@ -0,0 +1,25 @@
{
"name": "fingerprintjs2",
"description": "Modern & flexible browser fingerprinting library",
"main": "dist/fingerprint2.min.js",
"moduleType": [
"es6"
],
"keywords": [
"browser",
"fingerprint",
"fingerprinting",
"security",
"privacy"
],
"authors": [
"Valentin Vasilev"
],
"license": "MIT",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"spec"
]
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,31 @@
package {
import flash.display.Sprite;
import flash.display.LoaderInfo;
import flash.text.Font;
import flash.external.ExternalInterface;
public class FontList extends Sprite {
public function FontList() {
var params:Object = loadParams();
loadExternalInterface(params);
}
private function loadParams():Object {
return LoaderInfo(this.root.loaderInfo).parameters;
}
private function loadExternalInterface(params:Object):void {
ExternalInterface.call(params.onReady, fonts());
}
private function fonts():Array {
var fontNames:Array = [];
for each (var font:Font in Font.enumerateFonts(true) )
{
fontNames.push(font.fontName);
}
return fontNames;
}
}
}

View File

@ -0,0 +1,7 @@
all: FontList.swf
FontList.swf: clean
mxmlc -static-link-runtime-shared-libraries FontList.as
clean:
rm -f FontList.swf

View File

@ -0,0 +1,29 @@
var gulp = require("gulp"),
eslint = require("gulp-eslint"),
rename = require("gulp-rename"),
uglify = require("gulp-uglify");
gulp.task("lint", function() {
return gulp
.src("fingerprint2.js")
.pipe(eslint())
.pipe(eslint.format())
.pipe(eslint.failOnError());
});
gulp.task("minify", function() {
return gulp
.src("fingerprint2.js")
.pipe(rename({suffix: ".min"}))
.pipe(uglify({
compress: {
global_defs: {
NODEBUG: true
}
}
}))
.pipe(gulp.dest("dist/"));
});
gulp.task("default", ["lint", "minify"], function() {});

View File

@ -0,0 +1,98 @@
<!doctype html>
<html>
<head>
<title>Fingerprintjs2 test</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', 'UA-42202458-2', 'auto');
ga('send', 'pageview');
</script>
<script src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
<style>
body{
font-family: sans-serif;
max-width: 48em;
margin: auto;
padding: 0 5%;
background: #222;
color: #fff;
}
h1 {
margin: 2em 0 0;
}
p {
font-size: 1.2em
}
button {
border: none;
color: #fff;
font-size: 1.2em;
background: #27e;
padding: 0.5em 0.75em 0.6em;
border-radius: 3px;
box-shadow: 0 3px 0 #05c;
outline: none;
}
button:active {
transform: translateY(3px);
box-shadow: none;
}
strong {
display: block;
letter-spacing: 1px;
word-wrap: break-word;
}
@media (min-width: 32em) {
h1 {
font-size: 4em;
}
strong {
font-size: 1.5em;
}
}
</style>
</head>
<body>
<div id="container"></div>
<h1>Fingerprintjs2</h1>
<p>Your browser fingerprint: <strong id="fp"></strong></p>
<p><code id="time"/></p>
<button type="button" id="btn">Get my fingerprint</button>
<a href="https://github.com/Valve/fingerprintjs2"><img style="position: absolute; top: 0; right: 0; border: 0;" src="https://camo.githubusercontent.com/365986a132ccd6a44c23a9169022c0b5c890c387/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f7265645f6161303030302e706e67" alt="Fork me on GitHub" data-canonical-src="https://s3.amazonaws.com/github/ribbons/forkme_right_red_aa0000.png"></a>
<script src="fingerprint2.js"></script>
<script>
$("#btn").on("click", function () {
var d1 = new Date();
var fp = new Fingerprint2();
fp.get(function(result) {
var d2 = new Date();
var timeString = "Time took to calculate the fingerprint: " + (d2 - d1) + "ms";
if(typeof window.console !== "undefined") {
console.log(timeString);
console.log(result);
}
$("#fp").text(result);
$("#time").text(timeString);
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,33 @@
{
"name": "fingerprintjs2",
"version": "1.1.4",
"description": "Modern & flexible browser fingerprinting library",
"main": "dist/fingerprint2.min.js",
"devDependencies": {
"gulp": "^3.8.11",
"eslint": "^0.14.1",
"gulp-eslint": "^0.4.2",
"gulp-rename": "^1.2.2",
"gulp-uglify": "^1.1.0"
},
"scripts": {
"test": "specs/phantomjs.runner.sh specs/spec_runner.html"
},
"repository": {
"type": "git",
"url": "https://github.com/Valve/fingerprintjs2.git"
},
"keywords": [
"browser",
"identification",
"fingerprint",
"fingerprinting",
"privacy"
],
"author": "Valentin Vasilyev",
"license": "MIT",
"bugs": {
"url": "https://github.com/Valve/fingerprintjs2/issues"
},
"homepage": "https://github.com/Valve/fingerprintjs2"
}

View File

@ -0,0 +1,121 @@
/**
Starting with version 2.0, this file "boots" Jasmine, performing all of the necessary initialization before executing the loaded environment and all of a project's specs. This file should be loaded after `jasmine.js` and `jasmine_html.js`, but before any project source files or spec files are loaded. Thus this file can also be used to customize Jasmine for a project.
If a project is using Jasmine via the standalone distribution, this file can be customized directly. If a project is using Jasmine via the [Ruby gem][jasmine-gem], this file can be copied into the support directory via `jasmine copy_boot_js`. Other environments (e.g., Python) will have different mechanisms.
The location of `boot.js` can be specified and/or overridden in `jasmine.yml`.
[jasmine-gem]: http://github.com/pivotal/jasmine-gem
*/
(function() {
/**
* ## Require &amp; Instantiate
*
* Require Jasmine's core files. Specifically, this requires and attaches all of Jasmine's code to the `jasmine` reference.
*/
window.jasmine = jasmineRequire.core(jasmineRequire);
/**
* Since this is being run in a browser and the results should populate to an HTML page, require the HTML-specific Jasmine code, injecting the same reference.
*/
jasmineRequire.html(jasmine);
/**
* Create the Jasmine environment. This is used to run all specs in a project.
*/
var env = jasmine.getEnv();
/**
* ## The Global Interface
*
* Build up the functions that will be exposed as the Jasmine public interface. A project can customize, rename or alias any of these functions as desired, provided the implementation remains unchanged.
*/
var jasmineInterface = jasmineRequire.interface(jasmine, env);
/**
* Add all of the Jasmine global/public interface to the global scope, so a project can use the public interface directly. For example, calling `describe` in specs instead of `jasmine.getEnv().describe`.
*/
extend(window, jasmineInterface);
/**
* ## Runner Parameters
*
* More browser specific code - wrap the query string in an object and to allow for getting/setting parameters from the runner user interface.
*/
var queryString = new jasmine.QueryString({
getWindowLocation: function() { return window.location; }
});
var catchingExceptions = queryString.getParam("catch");
env.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions);
var throwingExpectationFailures = queryString.getParam("throwFailures");
env.throwOnExpectationFailure(throwingExpectationFailures);
/**
* ## Reporters
* The `HtmlReporter` builds all of the HTML UI for the runner page. This reporter paints the dots, stars, and x's for specs, as well as all spec names and all failures (if any).
*/
var htmlReporter = new jasmine.HtmlReporter({
env: env,
onRaiseExceptionsClick: function() { queryString.navigateWithNewParam("catch", !env.catchingExceptions()); },
onThrowExpectationsClick: function() { queryString.navigateWithNewParam("throwFailures", !env.throwingExpectationFailures()); },
addToExistingQueryString: function(key, value) { return queryString.fullStringWithNewParam(key, value); },
getContainer: function() { return document.body; },
createElement: function() { return document.createElement.apply(document, arguments); },
createTextNode: function() { return document.createTextNode.apply(document, arguments); },
timer: new jasmine.Timer()
});
/**
* The `jsApiReporter` also receives spec results, and is used by any environment that needs to extract the results from JavaScript.
*/
env.addReporter(jasmineInterface.jsApiReporter);
env.addReporter(htmlReporter);
/**
* Filter which specs will be run by matching the start of the full name against the `spec` query param.
*/
var specFilter = new jasmine.HtmlSpecFilter({
filterString: function() { return queryString.getParam("spec"); }
});
env.specFilter = function(spec) {
return specFilter.matches(spec.getFullName());
};
/**
* Setting up timing functions to be able to be overridden. Certain browsers (Safari, IE 8, phantomjs) require this hack.
*/
window.setTimeout = window.setTimeout;
window.setInterval = window.setInterval;
window.clearTimeout = window.clearTimeout;
window.clearInterval = window.clearInterval;
/**
* ## Execution
*
* Replace the browser window's `onload`, ensure it's called, and then run all of the loaded specs. This includes initializing the `HtmlReporter` instance and then executing the loaded Jasmine environment. All of this will happen after all of the specs are loaded.
*/
var currentWindowOnload = window.onload;
window.onload = function() {
if (currentWindowOnload) {
currentWindowOnload();
}
htmlReporter.initialize();
env.execute();
};
/**
* Helper function for readability above.
*/
function extend(destination, source) {
for (var property in source) destination[property] = source[property];
return destination;
}
}());

View File

@ -0,0 +1,446 @@
/*
Copyright (c) 2008-2015 Pivotal Labs
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
jasmineRequire.html = function(j$) {
j$.ResultsNode = jasmineRequire.ResultsNode();
j$.HtmlReporter = jasmineRequire.HtmlReporter(j$);
j$.QueryString = jasmineRequire.QueryString();
j$.HtmlSpecFilter = jasmineRequire.HtmlSpecFilter();
};
jasmineRequire.HtmlReporter = function(j$) {
var noopTimer = {
start: function() {},
elapsed: function() { return 0; }
};
function HtmlReporter(options) {
var env = options.env || {},
getContainer = options.getContainer,
createElement = options.createElement,
createTextNode = options.createTextNode,
onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {},
onThrowExpectationsClick = options.onThrowExpectationsClick || function() {},
addToExistingQueryString = options.addToExistingQueryString || defaultQueryString,
timer = options.timer || noopTimer,
results = [],
specsExecuted = 0,
failureCount = 0,
pendingSpecCount = 0,
htmlReporterMain,
symbols,
failedSuites = [];
this.initialize = function() {
clearPrior();
htmlReporterMain = createDom('div', {className: 'jasmine_html-reporter'},
createDom('div', {className: 'banner'},
createDom('a', {className: 'title', href: 'http://jasmine.github.io/', target: '_blank'}),
createDom('span', {className: 'version'}, j$.version)
),
createDom('ul', {className: 'symbol-summary'}),
createDom('div', {className: 'alert'}),
createDom('div', {className: 'results'},
createDom('div', {className: 'failures'})
)
);
getContainer().appendChild(htmlReporterMain);
symbols = find('.symbol-summary');
};
var totalSpecsDefined;
this.jasmineStarted = function(options) {
totalSpecsDefined = options.totalSpecsDefined || 0;
timer.start();
};
var summary = createDom('div', {className: 'summary'});
var topResults = new j$.ResultsNode({}, '', null),
currentParent = topResults;
this.suiteStarted = function(result) {
currentParent.addChild(result, 'suite');
currentParent = currentParent.last();
};
this.suiteDone = function(result) {
if (result.status == 'failed') {
failedSuites.push(result);
}
if (currentParent == topResults) {
return;
}
currentParent = currentParent.parent;
};
this.specStarted = function(result) {
currentParent.addChild(result, 'spec');
};
var failures = [];
this.specDone = function(result) {
if(noExpectations(result) && typeof console !== 'undefined' && typeof console.error !== 'undefined') {
console.error('Spec \'' + result.fullName + '\' has no expectations.');
}
if (result.status != 'disabled') {
specsExecuted++;
}
symbols.appendChild(createDom('li', {
className: noExpectations(result) ? 'empty' : result.status,
id: 'spec_' + result.id,
title: result.fullName
}
));
if (result.status == 'failed') {
failureCount++;
var failure =
createDom('div', {className: 'spec-detail failed'},
createDom('div', {className: 'description'},
createDom('a', {title: result.fullName, href: specHref(result)}, result.fullName)
),
createDom('div', {className: 'messages'})
);
var messages = failure.childNodes[1];
for (var i = 0; i < result.failedExpectations.length; i++) {
var expectation = result.failedExpectations[i];
messages.appendChild(createDom('div', {className: 'result-message'}, expectation.message));
messages.appendChild(createDom('div', {className: 'stack-trace'}, expectation.stack));
}
failures.push(failure);
}
if (result.status == 'pending') {
pendingSpecCount++;
}
};
this.jasmineDone = function() {
var banner = find('.banner');
var alert = find('.alert');
alert.appendChild(createDom('span', {className: 'duration'}, 'finished in ' + timer.elapsed() / 1000 + 's'));
banner.appendChild(
createDom('div', { className: 'run-options' },
createDom('span', { className: 'trigger' }, 'Options'),
createDom('div', { className: 'payload' },
createDom('div', { className: 'exceptions' },
createDom('input', {
className: 'raise',
id: 'raise-exceptions',
type: 'checkbox'
}),
createDom('label', { className: 'label', 'for': 'raise-exceptions' }, 'raise exceptions')),
createDom('div', { className: 'throw-failures' },
createDom('input', {
className: 'throw',
id: 'throw-failures',
type: 'checkbox'
}),
createDom('label', { className: 'label', 'for': 'throw-failures' }, 'stop spec on expectation failure'))
)
));
var raiseCheckbox = find('#raise-exceptions');
raiseCheckbox.checked = !env.catchingExceptions();
raiseCheckbox.onclick = onRaiseExceptionsClick;
var throwCheckbox = find('#throw-failures');
throwCheckbox.checked = env.throwingExpectationFailures();
throwCheckbox.onclick = onThrowExpectationsClick;
var optionsMenu = find('.run-options'),
optionsTrigger = optionsMenu.querySelector('.trigger'),
optionsPayload = optionsMenu.querySelector('.payload'),
isOpen = /\bopen\b/;
optionsTrigger.onclick = function() {
if (isOpen.test(optionsPayload.className)) {
optionsPayload.className = optionsPayload.className.replace(isOpen, '');
} else {
optionsPayload.className += ' open';
}
};
if (specsExecuted < totalSpecsDefined) {
var skippedMessage = 'Ran ' + specsExecuted + ' of ' + totalSpecsDefined + ' specs - run all';
alert.appendChild(
createDom('span', {className: 'bar skipped'},
createDom('a', {href: '?', title: 'Run all specs'}, skippedMessage)
)
);
}
var statusBarMessage = '';
var statusBarClassName = 'bar ';
if (totalSpecsDefined > 0) {
statusBarMessage += pluralize('spec', specsExecuted) + ', ' + pluralize('failure', failureCount);
if (pendingSpecCount) { statusBarMessage += ', ' + pluralize('pending spec', pendingSpecCount); }
statusBarClassName += (failureCount > 0) ? 'failed' : 'passed';
} else {
statusBarClassName += 'skipped';
statusBarMessage += 'No specs found';
}
alert.appendChild(createDom('span', {className: statusBarClassName}, statusBarMessage));
for(i = 0; i < failedSuites.length; i++) {
var failedSuite = failedSuites[i];
for(var j = 0; j < failedSuite.failedExpectations.length; j++) {
var errorBarMessage = 'AfterAll ' + failedSuite.failedExpectations[j].message;
var errorBarClassName = 'bar errored';
alert.appendChild(createDom('span', {className: errorBarClassName}, errorBarMessage));
}
}
var results = find('.results');
results.appendChild(summary);
summaryList(topResults, summary);
function summaryList(resultsTree, domParent) {
var specListNode;
for (var i = 0; i < resultsTree.children.length; i++) {
var resultNode = resultsTree.children[i];
if (resultNode.type == 'suite') {
var suiteListNode = createDom('ul', {className: 'suite', id: 'suite-' + resultNode.result.id},
createDom('li', {className: 'suite-detail'},
createDom('a', {href: specHref(resultNode.result)}, resultNode.result.description)
)
);
summaryList(resultNode, suiteListNode);
domParent.appendChild(suiteListNode);
}
if (resultNode.type == 'spec') {
if (domParent.getAttribute('class') != 'specs') {
specListNode = createDom('ul', {className: 'specs'});
domParent.appendChild(specListNode);
}
var specDescription = resultNode.result.description;
if(noExpectations(resultNode.result)) {
specDescription = 'SPEC HAS NO EXPECTATIONS ' + specDescription;
}
if(resultNode.result.status === 'pending' && resultNode.result.pendingReason !== '') {
specDescription = specDescription + ' PENDING WITH MESSAGE: ' + resultNode.result.pendingReason;
}
specListNode.appendChild(
createDom('li', {
className: resultNode.result.status,
id: 'spec-' + resultNode.result.id
},
createDom('a', {href: specHref(resultNode.result)}, specDescription)
)
);
}
}
}
if (failures.length) {
alert.appendChild(
createDom('span', {className: 'menu bar spec-list'},
createDom('span', {}, 'Spec List | '),
createDom('a', {className: 'failures-menu', href: '#'}, 'Failures')));
alert.appendChild(
createDom('span', {className: 'menu bar failure-list'},
createDom('a', {className: 'spec-list-menu', href: '#'}, 'Spec List'),
createDom('span', {}, ' | Failures ')));
find('.failures-menu').onclick = function() {
setMenuModeTo('failure-list');
};
find('.spec-list-menu').onclick = function() {
setMenuModeTo('spec-list');
};
setMenuModeTo('failure-list');
var failureNode = find('.failures');
for (var i = 0; i < failures.length; i++) {
failureNode.appendChild(failures[i]);
}
}
};
return this;
function find(selector) {
return getContainer().querySelector('.jasmine_html-reporter ' + selector);
}
function clearPrior() {
// return the reporter
var oldReporter = find('');
if(oldReporter) {
getContainer().removeChild(oldReporter);
}
}
function createDom(type, attrs, childrenVarArgs) {
var el = createElement(type);
for (var i = 2; i < arguments.length; i++) {
var child = arguments[i];
if (typeof child === 'string') {
el.appendChild(createTextNode(child));
} else {
if (child) {
el.appendChild(child);
}
}
}
for (var attr in attrs) {
if (attr == 'className') {
el[attr] = attrs[attr];
} else {
el.setAttribute(attr, attrs[attr]);
}
}
return el;
}
function pluralize(singular, count) {
var word = (count == 1 ? singular : singular + 's');
return '' + count + ' ' + word;
}
function specHref(result) {
return addToExistingQueryString('spec', result.fullName);
}
function defaultQueryString(key, value) {
return '?' + key + '=' + value;
}
function setMenuModeTo(mode) {
htmlReporterMain.setAttribute('class', 'jasmine_html-reporter ' + mode);
}
function noExpectations(result) {
return (result.failedExpectations.length + result.passedExpectations.length) === 0 &&
result.status === 'passed';
}
}
return HtmlReporter;
};
jasmineRequire.HtmlSpecFilter = function() {
function HtmlSpecFilter(options) {
var filterString = options && options.filterString() && options.filterString().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
var filterPattern = new RegExp(filterString);
this.matches = function(specName) {
return filterPattern.test(specName);
};
}
return HtmlSpecFilter;
};
jasmineRequire.ResultsNode = function() {
function ResultsNode(result, type, parent) {
this.result = result;
this.type = type;
this.parent = parent;
this.children = [];
this.addChild = function(result, type) {
this.children.push(new ResultsNode(result, type, this));
};
this.last = function() {
return this.children[this.children.length - 1];
};
}
return ResultsNode;
};
jasmineRequire.QueryString = function() {
function QueryString(options) {
this.navigateWithNewParam = function(key, value) {
options.getWindowLocation().search = this.fullStringWithNewParam(key, value);
};
this.fullStringWithNewParam = function(key, value) {
var paramMap = queryStringToParamMap();
paramMap[key] = value;
return toQueryString(paramMap);
};
this.getParam = function(key) {
return queryStringToParamMap()[key];
};
return this;
function toQueryString(paramMap) {
var qStrPairs = [];
for (var prop in paramMap) {
qStrPairs.push(encodeURIComponent(prop) + '=' + encodeURIComponent(paramMap[prop]));
}
return '?' + qStrPairs.join('&');
}
function queryStringToParamMap() {
var paramStr = options.getWindowLocation().search.substring(1),
params = [],
paramMap = {};
if (paramStr.length > 0) {
params = paramStr.split('&');
for (var i = 0; i < params.length; i++) {
var p = params[i].split('=');
var value = decodeURIComponent(p[1]);
if (value === 'true' || value === 'false') {
value = JSON.parse(value);
}
paramMap[decodeURIComponent(p[0])] = value;
}
}
return paramMap;
}
}
return QueryString;
};

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,261 @@
(function(global) {
var UNDEFINED,
exportObject;
if (typeof module !== "undefined" && module.exports) {
exportObject = exports;
} else {
exportObject = global.jasmineReporters = global.jasmineReporters || {};
}
function elapsed(start, end) { return (end - start)/1000; }
function isFailed(obj) { return obj.status === "failed"; }
function isSkipped(obj) { return obj.status === "pending"; }
function isDisabled(obj) { return obj.status === "disabled"; }
function extend(dupe, obj) { // performs a shallow copy of all props of `obj` onto `dupe`
for (var prop in obj) {
if (obj.hasOwnProperty(prop)) {
dupe[prop] = obj[prop];
}
}
return dupe;
}
function log(str) {
var con = global.console || console;
if (con && con.log && str && str.length) {
con.log(str);
}
}
/**
* Basic reporter that outputs spec results to the terminal.
* Use this reporter in your build pipeline.
*
* Usage:
*
* jasmine.getEnv().addReporter(new jasmineReporters.TerminalReporter(options);
*
* @param {object} [options]
* @param {number} [options.verbosity] meaningful values are 0 through 3; anything
* greater than 3 is treated as 3 (default: 2)
* @param {boolean} [options.color] print in color or not (default: true)
* @param {boolean} [opts.showStack] show stack trace for failed specs (default: false)
*/
var DEFAULT_VERBOSITY = 2,
ATTRIBUTES_TO_ANSI = {
"off": 0,
"bold": 1,
"red": 31,
"green": 32,
"yellow": 33,
"blue": 34,
"magenta": 35,
"cyan": 36
};
exportObject.TerminalReporter = function(options) {
var self = this;
self.started = false;
self.finished = false;
// sanitize arguments
options = options || {};
self.verbosity = typeof options.verbosity === "number" ? options.verbosity : DEFAULT_VERBOSITY;
self.color = options.color;
self.showStack = options.showStack;
var indent_string = ' ',
startTime,
currentSuite = null,
totalSpecsExecuted = 0,
totalSpecsSkipped = 0,
totalSpecsDisabled = 0,
totalSpecsFailed = 0,
totalSpecsDefined,
// when use use fit, jasmine never calls suiteStarted / suiteDone, so make a fake one to use
fakeFocusedSuite = {
id: 'focused',
description: 'focused specs',
fullName: 'focused specs'
};
var __suites = {}, __specs = {};
function getSuite(suite) {
__suites[suite.id] = extend(__suites[suite.id] || {}, suite);
return __suites[suite.id];
}
function getSpec(spec) {
__specs[spec.id] = extend(__specs[spec.id] || {}, spec);
return __specs[spec.id];
}
self.jasmineStarted = function(summary) {
totalSpecsDefined = summary && summary.totalSpecsDefined || NaN;
startTime = exportObject.startTime = new Date();
self.started = true;
};
self.suiteStarted = function(suite) {
suite = getSuite(suite);
suite._specs = 0;
suite._nestedSpecs = 0;
suite._failures = 0;
suite._nestedFailures = 0;
suite._skipped = 0;
suite._nestedSkipped = 0;
suite._disabled = 0;
suite._nestedDisabled = 0;
suite._depth = currentSuite ? currentSuite._depth+1 : 1;
suite._parent = currentSuite;
currentSuite = suite;
if (self.verbosity > 2) {
log(indentWithLevel(suite._depth, inColor(suite.description, "bold")));
}
};
self.specStarted = function(spec) {
if (!currentSuite) {
// focused spec (fit) -- suiteStarted was never called
self.suiteStarted(fakeFocusedSuite);
}
spec = getSpec(spec);
spec._suite = currentSuite;
spec._depth = currentSuite._depth+1;
currentSuite._specs++;
if (self.verbosity > 2) {
log(indentWithLevel(spec._depth, spec.description + ' ...'));
}
};
self.specDone = function(spec) {
spec = getSpec(spec);
var failed = false,
skipped = false,
disabled = false,
color = 'green',
resultText = '';
if (isSkipped(spec)) {
skipped = true;
color = '';
spec._suite._skipped++;
totalSpecsSkipped++;
}
if (isFailed(spec)) {
failed = true;
color = 'red';
spec._suite._failures++;
totalSpecsFailed++;
}
if (isDisabled(spec)) {
disabled = true;
color = 'yellow';
spec._suite._disabled++;
totalSpecsDisabled++;
}
totalSpecsExecuted++;
if (self.verbosity === 2) {
resultText = failed ? 'F' : skipped ? 'S' : disabled ? 'D' : '.';
} else if (self.verbosity > 2) {
resultText = ' ' + (failed ? 'Failed' : skipped ? 'Skipped' : disabled ? 'Disabled' : 'Passed');
}
log(inColor(resultText, color));
if (failed) {
if (self.verbosity === 1) {
log(spec.fullName);
} else if (self.verbosity === 2) {
log(' ');
log(indentWithLevel(spec._depth, spec.fullName));
}
for (var i = 0; i < spec.failedExpectations.length; i++) {
log(inColor(indentWithLevel(spec._depth, indent_string + spec.failedExpectations[i].message), color));
if (self.showStack){
logStackLines(spec._depth, spec.failedExpectations[i].stack.split('\n'));
}
}
}
};
self.suiteDone = function(suite) {
suite = getSuite(suite);
if (suite._parent === UNDEFINED) {
// disabled suite (xdescribe) -- suiteStarted was never called
self.suiteStarted(suite);
}
if (suite._parent) {
suite._parent._specs += suite._specs + suite._nestedSpecs;
suite._parent._failures += suite._failures + suite._nestedFailures;
suite._parent._skipped += suite._skipped + suite._nestedSkipped;
suite._parent._disabled += suite._disabled + suite._nestedDisabled;
}
currentSuite = suite._parent;
if (self.verbosity < 3) {
return;
}
var total = suite._specs + suite._nestedSpecs,
failed = suite._failures + suite._nestedFailures,
skipped = suite._skipped + suite._nestedSkipped,
disabled = suite._disabled + suite._nestedDisabled,
passed = total - failed - skipped,
color = failed ? 'red+bold' : 'green+bold',
str = passed + ' of ' + total + ' passed (' + skipped + ' skipped, ' + disabled + ' disabled)';
log(indentWithLevel(suite._depth, inColor(str+'.', color)));
};
self.jasmineDone = function() {
if (currentSuite) {
// focused spec (fit) -- suiteDone was never called
self.suiteDone(fakeFocusedSuite);
}
var now = new Date(),
dur = elapsed(startTime, now),
total = totalSpecsDefined || totalSpecsExecuted,
disabled = total - totalSpecsExecuted + totalSpecsDisabled,
skipped = totalSpecsSkipped,
spec_str = total + (total === 1 ? " spec, " : " specs, "),
fail_str = totalSpecsFailed + (totalSpecsFailed === 1 ? " failure, " : " failures, "),
skip_str = skipped + " skipped, ",
disabled_str = disabled + " disabled in ",
summary_str = spec_str + fail_str + skip_str + disabled_str + dur + "s.",
result_str = (totalSpecsFailed && "FAILURE: " || "SUCCESS: ") + summary_str,
result_color = totalSpecsFailed && "red+bold" || "green+bold";
if (self.verbosity === 2) {
log('');
}
if (self.verbosity > 0) {
log(inColor(result_str, result_color));
}
//log("Specs skipped but not reported (entire suite skipped or targeted to specific specs)", totalSpecsDefined - totalSpecsExecuted + totalSpecsDisabled);
self.finished = true;
// this is so phantomjs-testrunner.js can tell if we're done executing
exportObject.endTime = now;
};
function indentWithLevel(level, string) {
return new Array(level).join(indent_string) + string;
}
function logStackLines(depth, lines) {
lines.forEach(function(line){
log(inColor(indentWithLevel(depth, indent_string + line), 'magenta'));
});
}
function inColor(string, color) {
var color_attributes = color && color.split("+"),
ansi_string = "",
i;
if (!self.color || !color_attributes) {
return string;
}
for(i = 0; i < color_attributes.length; i++) {
ansi_string += "\033[" + ATTRIBUTES_TO_ANSI[color_attributes[i]] + "m";
}
ansi_string += string + "\033[" + ATTRIBUTES_TO_ANSI["off"] + "m";
return ansi_string;
}
};
})(this);

View File

@ -0,0 +1,230 @@
/* globals jasmineRequire, phantom */
// Verify arguments
var system = require('system');
var args;
if(phantom.args) {
args = phantom.args;
} else {
args = system.args.slice(1);//use system args for phantom 2.0+
}
if (args.length === 0) {
console.log("Simple JasmineBDD test runner for phantom.js");
console.log("Usage: phantomjs-testrunner.js url_to_runner.html");
console.log("Accepts http:// and file:// urls");
console.log("");
console.log("NOTE: This script depends on jasmine.HtmlReporter being used\non the page, for the DOM elements it creates.\n");
phantom.exit(2);
}
else {
var fs = require("fs"),
pages = [],
page, address, resultsKey, i, l;
var setupPageFn = function(p, k) {
return function() {
overloadPageEvaluate(p);
setupWriteFileFunction(p, k, fs.separator);
};
};
for (i = 0, l = args.length; i < l; i++) {
address = args[i];
console.log("Loading " + address);
// if provided a url without a protocol, try to use file://
address = address.indexOf("://") === -1 ? "file://" + address : address;
// create a WebPage object to work with
page = require("webpage").create();
page.url = address;
// When initialized, inject the reporting functions before the page is loaded
// (and thus before it will try to utilize the functions)
resultsKey = "__jr" + Math.ceil(Math.random() * 1000000);
page.onInitialized = setupPageFn(page, resultsKey);
page.open(address, processPage(null, page, resultsKey));
pages.push(page);
page.onConsoleMessage = logAndWorkAroundDefaultLineBreaking;
}
// bail when all pages have been processed
setInterval(function(){
var exit_code = 0;
for (i = 0, l = pages.length; i < l; i++) {
page = pages[i];
if (page.__exit_code === null) {
// wait until later
return;
}
exit_code |= page.__exit_code;
}
phantom.exit(exit_code);
}, 100);
}
// Thanks to hoisting, these helpers are still available when needed above
/**
* Logs a message. Does not add a line-break for single characters '.' and 'F' or lines ending in ' ...'
*
* @param msg
*/
function logAndWorkAroundDefaultLineBreaking(msg) {
var interpretAsWithoutNewline = /(^(\033\[\d+m)*[\.F](\033\[\d+m)*$)|( \.\.\.$)/;
if (navigator.userAgent.indexOf("Windows") < 0 && interpretAsWithoutNewline.test(msg)) {
try {
system.stdout.write(msg);
} catch (e) {
var fs = require('fs');
fs.write('/dev/stdout', msg, 'w');
}
} else {
console.log(msg);
}
}
/**
* Stringifies the function, replacing any %placeholders% with mapped values.
*
* @param {function} fn The function to replace occurrences within.
* @param {object} replacements Key => Value object of string replacements.
*/
function replaceFunctionPlaceholders(fn, replacements) {
if (replacements && typeof replacements === "object") {
fn = fn.toString();
for (var p in replacements) {
if (replacements.hasOwnProperty(p)) {
var match = new RegExp("%" + p + "%", "g");
do {
fn = fn.replace(match, replacements[p]);
} while(fn.indexOf(match) !== -1);
}
}
}
return fn;
}
/**
* Replaces the "evaluate" method with one we can easily do substitution with.
*
* @param {phantomjs.WebPage} page The WebPage object to overload
*/
function overloadPageEvaluate(page) {
page._evaluate = page.evaluate;
page.evaluate = function(fn, replacements) { return page._evaluate(replaceFunctionPlaceholders(fn, replacements)); };
return page;
}
/** Stubs a fake writeFile function into the test runner.
*
* @param {phantomjs.WebPage} page The WebPage object to inject functions into.
* @param {string} key The name of the global object in which file data should
* be stored for later retrieval.
*/
// TODO: not bothering with error checking for now (closed environment)
function setupWriteFileFunction(page, key, path_separator) {
page.evaluate(function(){
window["%resultsObj%"] = {};
window.fs_path_separator = "%fs_path_separator%";
window.__phantom_writeFile = function(filename, text) {
window["%resultsObj%"][filename] = text;
};
}, {resultsObj: key, fs_path_separator: path_separator.replace("\\", "\\\\")});
}
/**
* Returns the loaded page's filename => output object.
*
* @param {phantomjs.WebPage} page The WebPage object to retrieve data from.
* @param {string} key The name of the global object to be returned. Should
* be the same key provided to setupWriteFileFunction.
*/
function getXmlResults(page, key) {
return page.evaluate(function(){
return window["%resultsObj%"] || {};
}, {resultsObj: key});
}
/**
* Processes a page.
*
* @param {string} status The status from opening the page via WebPage#open.
* @param {phantomjs.WebPage} page The WebPage to be processed.
*/
function processPage(status, page, resultsKey) {
if (status === null && page) {
page.__exit_code = null;
return function(stat){
processPage(stat, page, resultsKey);
};
}
if (status !== "success") {
console.error("Unable to load resource: " + address);
page.__exit_code = 2;
}
else {
var isFinished = function() {
return page.evaluate(function(){
// if there's a JUnitXmlReporter, return a boolean indicating if it is finished
if (window.jasmineReporters && window.jasmineReporters.startTime) {
return !!window.jasmineReporters.endTime;
}
// otherwise, scrape the DOM for the HtmlReporter "finished in ..." output
var durElem = document.querySelector(".html-reporter .duration");
if (!durElem) {
durElem = document.querySelector(".jasmine_html-reporter .duration");
}
return durElem && durElem.textContent && durElem.textContent.toLowerCase().indexOf("finished in") === 0;
});
};
var getResultsFromHtmlRunner = function() {
return page.evaluate(function(){
var resultElem = document.querySelector(".html-reporter .alert .bar");
if (!resultElem) {
resultElem = document.querySelector(".jasmine_html-reporter .alert .bar");
}
return resultElem && resultElem.textContent &&
resultElem.textContent.match(/(\d+) spec.* (\d+) failure.*/) ||
["Unable to determine success or failure."];
});
};
var timeout = 60000;
var loopInterval = 100;
var ival = setInterval(function(){
if (isFinished()) {
// get the results that need to be written to disk
var fs = require("fs"),
xml_results = getXmlResults(page, resultsKey),
output;
for (var filename in xml_results) {
if (xml_results.hasOwnProperty(filename) && (output = xml_results[filename]) && typeof(output) === "string") {
fs.write(filename, output, "w");
}
}
// print out a success / failure message of the results
var results = getResultsFromHtmlRunner();
var failures = Number(results[2]);
if (failures > 0) {
page.__exit_code = 1;
clearInterval(ival);
}
else {
page.__exit_code = 0;
clearInterval(ival);
}
}
else {
timeout -= loopInterval;
if (timeout <= 0) {
console.log('Page has timed out; aborting.');
page.__exit_code = 2;
clearInterval(ival);
}
}
}, loopInterval);
}
}

View File

@ -0,0 +1,40 @@
#!/bin/bash
# sanity check to make sure phantomjs exists in the PATH
hash /usr/bin/env phantomjs &> /dev/null
if [ $? -eq 1 ]; then
echo "ERROR: phantomjs is not installed"
echo "Please visit http://www.phantomjs.org/"
exit 1
fi
# sanity check number of args
if [ $# -lt 1 ]
then
echo "Usage: `basename $0` path_to_runner.html"
echo
exit 1
fi
SCRIPTDIR=$(dirname `perl -e 'use Cwd "abs_path";print abs_path(shift)' $0`)
TESTFILE=""
while (( "$#" )); do
if [ ${1:0:7} == "http://" -o ${1:0:8} == "https://" ]; then
TESTFILE="$TESTFILE $1"
else
TESTFILE="$TESTFILE `perl -e 'use Cwd "abs_path";print abs_path(shift)' $1`"
fi
shift
done
# cleanup previous test runs
cd $SCRIPTDIR
rm -f *.xml
# make sure phantomjs submodule is initialized
cd ..
git submodule update --init
# fire up the phantomjs environment and run the test
cd $SCRIPTDIR
/usr/bin/env phantomjs $SCRIPTDIR/phantomjs-testrunner.js $TESTFILE

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fingerprint2 spec runner</title>
<link rel="shortcut icon" type="image/png" href="lib/jasmine-2.3.4/jasmine_favicon.png">
<link rel="stylesheet" href="lib/jasmine-2.3.4/jasmine.css">
<script src="lib/jasmine-2.3.4/jasmine.js"></script>
<script src="lib/jasmine-2.3.4/jasmine-html.js"></script>
<script src="lib/jasmine-2.3.4/boot.js"></script>
<script src="lib/jasmine-2.3.4/terminal.js"></script>
<script src="lib/jasmine-2.3.4/jasmine-matchers.js"></script>
<!-- include source files here... -->
<script src="../fingerprint2.js"></script>
<!-- include spec files here... -->
<script src="./specs.js"></script>
<script>
if(navigator.userAgent.match(/phantom/i)) {
jasmine.getEnv().addReporter(new jasmineReporters.TerminalReporter({
verbosity: 3,
color: true,
showStack: true
}));
}
</script>
</head>
<body>
</body>
</html>

View File

@ -0,0 +1,180 @@
"use strict";
describe("Fingerprint2", function () {
describe("new", function () {
it("creates a new instance of FP2", function () {
expect(new Fingerprint2()).not.toBeNull();
});
it("accepts an empty options object", function () {
expect(new Fingerprint2({})).not.toBeNull();
});
it("uses default options", function () {
var fp2 = new Fingerprint2();
expect(fp2.options.swfContainerId).toEqual("fingerprintjs2");
expect(fp2.options.swfPath).toEqual("flash/compiled/FontList.swf");
});
it("allows to override default options", function () {
var fp2 = new Fingerprint2({swfPath: "newpath"});
expect(fp2.options.swfContainerId).toEqual("fingerprintjs2");
expect(fp2.options.swfPath).toEqual("newpath");
});
it("allows to add new options", function () {
var fp2 = new Fingerprint2({excludeUserAgent: true});
expect(fp2.options.swfContainerId).toEqual("fingerprintjs2");
expect(fp2.options.swfPath).toEqual("flash/compiled/FontList.swf");
expect(fp2.options.excludeUserAgent).toBe(true);
});
describe("sortPluginsFor", function () {
it("has default value", function (){
var fp2 = new Fingerprint2();
expect(fp2.options.sortPluginsFor).toEqual([/palemoon/i]);
});
it("allows to set new array of regexes", function () {
var fp2 = new Fingerprint2({sortPluginsFor: [/firefox/i, /chrome/i]});
expect(fp2.options.sortPluginsFor).toEqual([/firefox/i, /chrome/i]);
});
});
});
describe("get", function () {
describe("default options", function () {
it("calculates fingerprint", function (done) {
var fp2 = new Fingerprint2();
fp2.get(function(result){
expect(result).toMatch(/^[0-9a-f]{32}$/i);
done();
});
});
it("does not try calling flash font detection", function (done) {
var fp2 = new Fingerprint2();
spyOn(fp2, "flashFontsKey");
fp2.get(function(result) {
expect(fp2.flashFontsKey).not.toHaveBeenCalled();
done();
});
});
});
describe("non-default options", function () {
it("does not use userAgent when excluded", function (done) {
var fp2 = new Fingerprint2({excludeUserAgent: true});
spyOn(fp2, "getUserAgent");
fp2.get(function(result) {
expect(fp2.getUserAgent).not.toHaveBeenCalled();
done();
});
});
it("does not use screen resolution when excluded", function (done) {
var fp2 = new Fingerprint2({excludeScreenResolution: true});
spyOn(fp2, "getScreenResolution");
fp2.get(function(result) {
expect(fp2.getScreenResolution).not.toHaveBeenCalled();
done();
});
});
it("does not use available screen resolution when excluded", function (done) {
var fp2 = new Fingerprint2({excludeAvailableScreenResolution: true});
spyOn(fp2, "getAvailableScreenResolution");
fp2.get(function(result) {
expect(fp2.getAvailableScreenResolution).not.toHaveBeenCalled();
done();
});
});
it("does not use plugins info when excluded", function (done) {
var fp2 = new Fingerprint2({excludePlugins: true});
spyOn(fp2, "getRegularPlugins");
fp2.get(function(result) {
expect(fp2.getRegularPlugins).not.toHaveBeenCalled();
done();
});
});
it("does not use IE plugins info when excluded", function (done) {
var fp2 = new Fingerprint2({excludeIEPlugins: true});
spyOn(fp2, "getIEPlugins");
fp2.get(function(result) {
expect(fp2.getIEPlugins).not.toHaveBeenCalled();
done();
});
});
});
describe("returns components", function () {
it("does it return components as a second argument to callback", function (done) {
var fp2 = new Fingerprint2();
fp2.get(function(result, components) {
expect(components).not.toBeNull();
done();
});
});
it("checks if returned components is array", function (done) {
var fp2 = new Fingerprint2();
fp2.get(function(result, components) {
expect(components).toBeArrayOfObjects();
done();
});
});
it("checks if js_fonts component is array", function (done) {
var fp2 = new Fingerprint2();
fp2.get(function(result, components) {
for(var x = 0; x < components.length; x++) {
if(components[x].key == "js_fonts") {
expect(components[x].value).toBeArray();
}
}
done();
});
});
it("returns user_agent as the first element", function (done) {
var fp2 = new Fingerprint2();
fp2.get(function(result, components) {
expect(components[0].key).toEqual("user_agent");
done();
});
});
});
describe("baseFontArray iteration", function () {
it("only iterates specified items", function (done) {
var baseFonts = ["monospace", "sans-serif", "serif"];
var ctr = 0;
for (var x in baseFonts) {
ctr++;
}
expect(baseFonts.length).toEqual(3);
expect(ctr).toEqual(baseFonts.length);
// Somewhere deep in your JavaScript library...
Array.prototype.foo = 1;
Array.prototype.bar = 2;
ctr = 0;
for (var x in baseFonts) {
console.log(x);
ctr++;
// Now foo & bar is a part of EVERY array and
// will show up here as a value of 'x'.
}
expect(baseFonts.length).toEqual(3);
// sadface
expect(ctr).not.toEqual(baseFonts.length);
expect(ctr).toEqual(5);
done();
});
});
});
});

View File

@ -14,14 +14,14 @@
"package.json" "package.json"
], ],
"homepage": "https://github.com/hammerjs/hammer.js", "homepage": "https://github.com/hammerjs/hammer.js",
"version": "2.0.6", "version": "2.0.8",
"_release": "2.0.6", "_release": "2.0.8",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "v2.0.6", "tag": "v2.0.8",
"commit": "05f0872c6130daf537bad27a393310cd5833c9a1" "commit": "ee611316bec077fcfbba3fd604ebc4b0b35ac288"
}, },
"_source": "git://github.com/hammerjs/hammer.js.git", "_source": "https://github.com/hammerjs/hammer.js.git",
"_target": "~2.0.4", "_target": "~2.0.4",
"_originalSource": "hammer.js" "_originalSource": "hammer.js"
} }

View File

@ -1,8 +1,8 @@
/*! Hammer.JS - v2.0.6 - 2015-12-23 /*! Hammer.JS - v2.0.7 - 2016-04-22
* http://hammerjs.github.io/ * http://hammerjs.github.io/
* *
* Copyright (c) 2015 Jorik Tangelder; * Copyright (c) 2016 Jorik Tangelder;
* Licensed under the license */ * Licensed under the MIT license */
(function(window, document, exportName, undefined) { (function(window, document, exportName, undefined) {
'use strict'; 'use strict';
@ -130,7 +130,7 @@ if (typeof Object.assign !== 'function') {
* means that properties in dest will be overwritten by the ones in src. * means that properties in dest will be overwritten by the ones in src.
* @param {Object} dest * @param {Object} dest
* @param {Object} src * @param {Object} src
* @param {Boolean=false} [merge] * @param {Boolean} [merge=false]
* @returns {Object} dest * @returns {Object} dest
*/ */
var extend = deprecate(function extend(dest, src, merge) { var extend = deprecate(function extend(dest, src, merge) {
@ -791,7 +791,6 @@ function MouseInput() {
this.evEl = MOUSE_ELEMENT_EVENTS; this.evEl = MOUSE_ELEMENT_EVENTS;
this.evWin = MOUSE_WINDOW_EVENTS; this.evWin = MOUSE_WINDOW_EVENTS;
this.allow = true; // used by Input.TouchMouse to disable mouse events
this.pressed = false; // mousedown state this.pressed = false; // mousedown state
Input.apply(this, arguments); Input.apply(this, arguments);
@ -814,8 +813,8 @@ inherit(MouseInput, Input, {
eventType = INPUT_END; eventType = INPUT_END;
} }
// mouse must be down, and mouse events are allowed (see the TouchMouse input) // mouse must be down
if (!this.pressed || !this.allow) { if (!this.pressed) {
return; return;
} }
@ -1098,12 +1097,19 @@ function getTouches(ev, type) {
* @constructor * @constructor
* @extends Input * @extends Input
*/ */
var DEDUP_TIMEOUT = 2500;
var DEDUP_DISTANCE = 25;
function TouchMouseInput() { function TouchMouseInput() {
Input.apply(this, arguments); Input.apply(this, arguments);
var handler = bindFn(this.handler, this); var handler = bindFn(this.handler, this);
this.touch = new TouchInput(this.manager, handler); this.touch = new TouchInput(this.manager, handler);
this.mouse = new MouseInput(this.manager, handler); this.mouse = new MouseInput(this.manager, handler);
this.primaryTouch = null;
this.lastTouches = [];
} }
inherit(TouchMouseInput, Input, { inherit(TouchMouseInput, Input, {
@ -1117,17 +1123,15 @@ inherit(TouchMouseInput, Input, {
var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH), var isTouch = (inputData.pointerType == INPUT_TYPE_TOUCH),
isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE); isMouse = (inputData.pointerType == INPUT_TYPE_MOUSE);
// when we're in a touch event, so block all upcoming mouse events if (isMouse && inputData.sourceCapabilities && inputData.sourceCapabilities.firesTouchEvents) {
// most mobile browser also emit mouseevents, right after touchstart
if (isTouch) {
this.mouse.allow = false;
} else if (isMouse && !this.mouse.allow) {
return; return;
} }
// reset the allowMouse when we're done // when we're in a touch event, record touches to de-dupe synthetic mouse event
if (inputEvent & (INPUT_END | INPUT_CANCEL)) { if (isTouch) {
this.mouse.allow = true; recordTouches.call(this, inputEvent, inputData);
} else if (isMouse && isSyntheticEvent.call(this, inputData)) {
return;
} }
this.callback(manager, inputEvent, inputData); this.callback(manager, inputEvent, inputData);
@ -1142,6 +1146,44 @@ inherit(TouchMouseInput, Input, {
} }
}); });
function recordTouches(eventType, eventData) {
if (eventType & INPUT_START) {
this.primaryTouch = eventData.changedPointers[0].identifier;
setLastTouch.call(this, eventData);
} else if (eventType & (INPUT_END | INPUT_CANCEL)) {
setLastTouch.call(this, eventData);
}
}
function setLastTouch(eventData) {
var touch = eventData.changedPointers[0];
if (touch.identifier === this.primaryTouch) {
var lastTouch = {x: touch.clientX, y: touch.clientY};
this.lastTouches.push(lastTouch);
var lts = this.lastTouches;
var removeLastTouch = function() {
var i = lts.indexOf(lastTouch);
if (i > -1) {
lts.splice(i, 1);
}
};
setTimeout(removeLastTouch, DEDUP_TIMEOUT);
}
}
function isSyntheticEvent(eventData) {
var x = eventData.srcEvent.clientX, y = eventData.srcEvent.clientY;
for (var i = 0; i < this.lastTouches.length; i++) {
var t = this.lastTouches[i];
var dx = Math.abs(x - t.x), dy = Math.abs(y - t.y);
if (dx <= DEDUP_DISTANCE && dy <= DEDUP_DISTANCE) {
return true;
}
}
return false;
}
var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction'); var PREFIXED_TOUCH_ACTION = prefixed(TEST_ELEMENT.style, 'touchAction');
var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined; var NATIVE_TOUCH_ACTION = PREFIXED_TOUCH_ACTION !== undefined;
@ -1152,6 +1194,7 @@ var TOUCH_ACTION_MANIPULATION = 'manipulation'; // not implemented
var TOUCH_ACTION_NONE = 'none'; var TOUCH_ACTION_NONE = 'none';
var TOUCH_ACTION_PAN_X = 'pan-x'; var TOUCH_ACTION_PAN_X = 'pan-x';
var TOUCH_ACTION_PAN_Y = 'pan-y'; var TOUCH_ACTION_PAN_Y = 'pan-y';
var TOUCH_ACTION_MAP = getTouchActionProps();
/** /**
* Touch Action * Touch Action
@ -1176,7 +1219,7 @@ TouchAction.prototype = {
value = this.compute(); value = this.compute();
} }
if (NATIVE_TOUCH_ACTION && this.manager.element.style) { if (NATIVE_TOUCH_ACTION && this.manager.element.style && TOUCH_ACTION_MAP[value]) {
this.manager.element.style[PREFIXED_TOUCH_ACTION] = value; this.manager.element.style[PREFIXED_TOUCH_ACTION] = value;
} }
this.actions = value.toLowerCase().trim(); this.actions = value.toLowerCase().trim();
@ -1208,11 +1251,6 @@ TouchAction.prototype = {
* @param {Object} input * @param {Object} input
*/ */
preventDefaults: function(input) { preventDefaults: function(input) {
// not needed with native support for the touchAction property
if (NATIVE_TOUCH_ACTION) {
return;
}
var srcEvent = input.srcEvent; var srcEvent = input.srcEvent;
var direction = input.offsetDirection; var direction = input.offsetDirection;
@ -1223,9 +1261,9 @@ TouchAction.prototype = {
} }
var actions = this.actions; var actions = this.actions;
var hasNone = inStr(actions, TOUCH_ACTION_NONE); var hasNone = inStr(actions, TOUCH_ACTION_NONE) && !TOUCH_ACTION_MAP[TOUCH_ACTION_NONE];
var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y); var hasPanY = inStr(actions, TOUCH_ACTION_PAN_Y) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_Y];
var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X); var hasPanX = inStr(actions, TOUCH_ACTION_PAN_X) && !TOUCH_ACTION_MAP[TOUCH_ACTION_PAN_X];
if (hasNone) { if (hasNone) {
//do not prevent defaults if this is a tap gesture //do not prevent defaults if this is a tap gesture
@ -1296,6 +1334,21 @@ function cleanTouchActions(actions) {
return TOUCH_ACTION_AUTO; return TOUCH_ACTION_AUTO;
} }
function getTouchActionProps() {
if (!NATIVE_TOUCH_ACTION) {
return false;
}
var touchMap = {};
var cssSupports = window.CSS && window.CSS.supports;
['auto', 'manipulation', 'pan-y', 'pan-x', 'pan-x pan-y', 'none'].forEach(function(val) {
// If css.supports is not supported but there is native touch-action assume it supports
// all values. This is the case for IE 10 and 11.
touchMap[val] = cssSupports ? window.CSS.supports('touch-action', val) : true;
});
return touchMap;
}
/** /**
* Recognizer flow explained; * * Recognizer flow explained; *
* All recognizers have the initial state of POSSIBLE when a input session starts. * All recognizers have the initial state of POSSIBLE when a input session starts.
@ -2092,7 +2145,7 @@ function Hammer(element, options) {
/** /**
* @const {string} * @const {string}
*/ */
Hammer.VERSION = '2.0.6'; Hammer.VERSION = '2.0.7';
/** /**
* default settings * default settings
@ -2223,6 +2276,7 @@ function Manager(element, options) {
this.handlers = {}; this.handlers = {};
this.session = {}; this.session = {};
this.recognizers = []; this.recognizers = [];
this.oldCssProps = {};
this.element = element; this.element = element;
this.input = createInputInstance(this); this.input = createInputInstance(this);
@ -2401,6 +2455,13 @@ Manager.prototype = {
* @returns {EventEmitter} this * @returns {EventEmitter} this
*/ */
on: function(events, handler) { on: function(events, handler) {
if (events === undefined) {
return;
}
if (handler === undefined) {
return;
}
var handlers = this.handlers; var handlers = this.handlers;
each(splitStr(events), function(event) { each(splitStr(events), function(event) {
handlers[event] = handlers[event] || []; handlers[event] = handlers[event] || [];
@ -2416,6 +2477,10 @@ Manager.prototype = {
* @returns {EventEmitter} this * @returns {EventEmitter} this
*/ */
off: function(events, handler) { off: function(events, handler) {
if (events === undefined) {
return;
}
var handlers = this.handlers; var handlers = this.handlers;
each(splitStr(events), function(event) { each(splitStr(events), function(event) {
if (!handler) { if (!handler) {
@ -2480,9 +2545,19 @@ function toggleCssProps(manager, add) {
if (!element.style) { if (!element.style) {
return; return;
} }
var prop;
each(manager.options.cssProps, function(value, name) { each(manager.options.cssProps, function(value, name) {
element.style[prefixed(element.style, name)] = add ? value : ''; prop = prefixed(element.style, name);
if (add) {
manager.oldCssProps[prop] = element.style[prop];
element.style[prop] = value;
} else {
element.style[prop] = manager.oldCssProps[prop] || '';
}
}); });
if (!add) {
manager.oldCssProps = {};
}
} }
/** /**

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{ {
"name": "hls.js", "name": "hls.js",
"version": "0.5.22", "version": "0.5.24",
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "Media Source Extension - HLS library, by/for Dailymotion", "description": "Media Source Extension - HLS library, by/for Dailymotion",
"homepage": "https://github.com/dailymotion/hls.js", "homepage": "https://github.com/dailymotion/hls.js",
@ -16,11 +16,11 @@
"test", "test",
"tests" "tests"
], ],
"_release": "0.5.22", "_release": "0.5.24",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "v0.5.22", "tag": "v0.5.24",
"commit": "ed41f6bffac7c6e35963c8aa741e00be8edea2c8" "commit": "276d45148e6306e9ae06f4c9fa5778e723b78d05"
}, },
"_source": "git://github.com/dailymotion/hls.js.git", "_source": "git://github.com/dailymotion/hls.js.git",
"_target": "~0.5.7", "_target": "~0.5.7",

View File

@ -86,26 +86,12 @@ each error is categorized by :
- ```Hls.ErrorTypes.MEDIA_ERROR```for media/video related errors - ```Hls.ErrorTypes.MEDIA_ERROR```for media/video related errors
- ```Hls.ErrorTypes.OTHER_ERROR```for all other errors - ```Hls.ErrorTypes.OTHER_ERROR```for all other errors
- its details: - its details:
- ```Hls.ErrorDetails.MANIFEST_LOAD_ERROR```raised when manifest loading fails because of a network error - refer to [Errors details](#Errors)
- ```Hls.ErrorDetails.MANIFEST_LOAD_TIMEOUT```raised when manifest loading fails because of a timeout
- ```Hls.ErrorDetails.MANIFEST_PARSING_ERROR```raised when manifest parsing failed to find proper content
- ```Hls.ErrorDetails.LEVEL_LOAD_ERROR```raised when level loading fails because of a network error
- ```Hls.ErrorDetails.LEVEL_LOAD_TIMEOUT```raised when level loading fails because of a timeout
- ```Hls.ErrorDetails.LEVEL_SWITCH_ERROR```raised when level switching fails
- ```Hls.ErrorDetails.FRAG_LOAD_ERROR```raised when fragment loading fails because of a network error
- ```Hls.ErrorDetails.FRAG_LOOP_LOADING_ERROR```raised upon detection of same fragment being requested in loop
- ```Hls.ErrorDetails.FRAG_LOAD_TIMEOUT```raised when fragment loading fails because of a timeout
- ```Hls.ErrorDetails.FRAG_DECRYPT_ERROR```raised when fragment decryption fails
- ```Hls.ErrorDetails.FRAG_PARSING_ERROR```raised when fragment parsing fails
- ```Hls.ErrorDetails.BUFFER_ADD_CODEC_ERROR```raised when exception is raised while adding a new sourceBuffer to MediaSource
- ```Hls.ErrorDetails.BUFFER_APPEND_ERROR```raised when exception is raised while preparing buffer append
- ```Hls.ErrorDetails.BUFFER_APPENDING_ERROR```raised when exception is raised during buffer appending
- ```Hls.ErrorDetails.BUFFER_STALLED_ERROR```raised when playback stalls because the buffer runs out
- its fatality: - its fatality:
- ```false```if error is not fatal, hls.js will try to recover it - ```false```if error is not fatal, hls.js will try to recover it
- ```true```if error is fatal, an action is required to (try to) recover it. - ```true```if error is fatal, an action is required to (try to) recover it.
full details is described [below](##Errors) full details is described [below](#Errors)
see sample code below to listen to errors: see sample code below to listen to errors:
@ -560,10 +546,6 @@ get/set : capping/max level value that could be used by ABR Controller
default value is -1 (no level capping) default value is -1 (no level capping)
## Version Control
#### ```Hls.version```
static getter: return hls.js dist version number
## Network Loading Control API ## Network Loading Control API
@ -620,7 +602,7 @@ full list of Events available below :
- `Hls.Events.FRAG_LOADING` - fired when a fragment loading starts - `Hls.Events.FRAG_LOADING` - fired when a fragment loading starts
- data: { frag : fragment object} - data: { frag : fragment object}
- `Hls.Events.FRAG_LOAD_PROGRESS` - fired when a fragment load is in progress - `Hls.Events.FRAG_LOAD_PROGRESS` - fired when a fragment load is in progress
- data: { frag : fragment object with frag.loaded=stats.loaded, stats : { trequest, tfirst, loaded, total} } - data: { frag : fragment object with frag.loaded=stats.loaded, stats : { trequest, tfirst, loaded} }
- `Hls.Events.FRAG_LOADED` - fired when a fragment loading is completed - `Hls.Events.FRAG_LOADED` - fired when a fragment loading is completed
- data: { frag : fragment object, payload : fragment payload, stats : { trequest, tfirst, tload, length}} - data: { frag : fragment object, payload : fragment payload, stats : { trequest, tfirst, tload, length}}
- `Hls.Events.FRAG_PARSING_INIT_SEGMENT` - fired when Init Segment has been extracted from fragment - `Hls.Events.FRAG_PARSING_INIT_SEGMENT` - fired when Init Segment has been extracted from fragment
@ -637,8 +619,6 @@ full list of Events available below :
- data: { frag : fragment object } - data: { frag : fragment object }
- `Hls.Events.FPS_DROP` - triggered when FPS drop in last monitoring period is higher than given threshold - `Hls.Events.FPS_DROP` - triggered when FPS drop in last monitoring period is higher than given threshold
- data: {curentDropped : nb of dropped frames in last monitoring period, currentDecoded: nb of decoded frames in last monitoring period, totalDropped : total dropped frames on this video element} - data: {curentDropped : nb of dropped frames in last monitoring period, currentDecoded: nb of decoded frames in last monitoring period, totalDropped : total dropped frames on this video element}
- `Hls.Events.FPS_DROP_LEVEL_CAPPING` - triggered when FPS drop triggers auto level capping
- data: { level: suggested new auto level capping by fps controller, droppedLevel : level has to much dropped frame will be restricted }
- `Hls.Events.ERROR` - Identifier for an error event - `Hls.Events.ERROR` - Identifier for an error event
- data: { type : error Type, details : error details, fatal : is error fatal or not, other error specific data} - data: { type : error Type, details : error details, fatal : is error fatal or not, other error specific data}
- `Hls.Events.DESTROYING` - fired when hls.js instance starts destroying. Different from MEDIA_DETACHED as one could want to detach and reattach a video to the instance of hls.js to handle mid-rolls for example. - `Hls.Events.DESTROYING` - fired when hls.js instance starts destroying. Different from MEDIA_DETACHED as one could want to detach and reattach a video to the instance of hls.js to handle mid-rolls for example.
@ -676,8 +656,6 @@ full list of Errors is described below:
### Media Errors ### Media Errors
- ```Hls.ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR```raised when manifest only contains quality level with codecs incompatible with MediaSource Engine. - ```Hls.ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR```raised when manifest only contains quality level with codecs incompatible with MediaSource Engine.
- data: { type : ```MEDIA_ERROR```, details : ```Hls.ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR```, fatal : ```true```, url : manifest URL} - data: { type : ```MEDIA_ERROR```, details : ```Hls.ErrorDetails.MANIFEST_INCOMPATIBLE_CODECS_ERROR```, fatal : ```true```, url : manifest URL}
- ```Hls.ErrorDetails.BUFFER_ADD_CODEC_ERROR```raised when MediaSource fails to add new sourceBuffer
- data: { type : ```MEDIA_ERROR```, details : ```Hls.ErrorDetails.BUFFER_ADD_CODEC_ERROR```, fatal : ```false```, err : error raised by MediaSource, mimeType: mimeType on which the failure happened}
- ```Hls.ErrorDetails.BUFFER_APPEND_ERROR```raised when exception is raised while calling buffer append - ```Hls.ErrorDetails.BUFFER_APPEND_ERROR```raised when exception is raised while calling buffer append
- data: { type : ```MEDIA_ERROR```, details : ```Hls.ErrorDetails.BUFFER_APPEND_ERROR```, fatal : ```true```, frag : fragment object} - data: { type : ```MEDIA_ERROR```, details : ```Hls.ErrorDetails.BUFFER_APPEND_ERROR```, fatal : ```true```, frag : fragment object}
- ```Hls.ErrorDetails.BUFFER_APPENDING_ERROR```raised when exception is raised during buffer appending - ```Hls.ErrorDetails.BUFFER_APPENDING_ERROR```raised when exception is raised during buffer appending
@ -687,7 +665,9 @@ full list of Errors is described below:
- ```Hls.ErrorDetails.BUFFER_FULL_ERROR```raised when no data can be appended anymore in media buffer because it is full. this error is recovered automatically by performing a smooth level switching that empty buffers (without disrupting the playback) and reducing the max buffer length. - ```Hls.ErrorDetails.BUFFER_FULL_ERROR```raised when no data can be appended anymore in media buffer because it is full. this error is recovered automatically by performing a smooth level switching that empty buffers (without disrupting the playback) and reducing the max buffer length.
- data: { type : ```MEDIA_ERROR```, details : ```Hls.ErrorDetails.BUFFER_FULL_ERROR```, fatal : ```false```} - data: { type : ```MEDIA_ERROR```, details : ```Hls.ErrorDetails.BUFFER_FULL_ERROR```, fatal : ```false```}
- ```Hls.ErrorDetails.BUFFER_SEEK_OVER_HOLE```raised after hls.js seeks over a buffer hole to unstuck the playback, - ```Hls.ErrorDetails.BUFFER_SEEK_OVER_HOLE```raised after hls.js seeks over a buffer hole to unstuck the playback,
- data: { type : ```MEDIA_ERROR```, details : ```Hls.ErrorDetails.BUFFER_SEEK_OVER_HOLE```, fatal : ```false```} - data: { type : ```MEDIA_ERROR```, details : ```Hls.ErrorDetails.BUFFER_SEEK_OVER_HOLE```, fatal : ```false```, hole : hole duration}
- ```Hls.ErrorDetails.BUFFER_SEEK_STUCK_IN_BUFFERED```raised after hls.js seeks to workaround a playback stuck although currentTime is buffered
- data: { type : ```MEDIA_ERROR```, details : ```Hls.ErrorDetails.BUFFER_SEEK_STUCK_IN_BUFFERED```, fatal : ```false```}
## Objects ## Objects
### Level ### Level

View File

@ -1,6 +1,6 @@
{ {
"name": "hls.js", "name": "hls.js",
"version": "0.6.1", "version": "0.5.24",
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "Media Source Extension - HLS library, by/for Dailymotion", "description": "Media Source Extension - HLS library, by/for Dailymotion",
"homepage": "https://github.com/dailymotion/hls.js", "homepage": "https://github.com/dailymotion/hls.js",

View File

@ -93,7 +93,7 @@ header {
<video id="video" controls autoplay class="videoCentered"></video><br> <video id="video" controls autoplay class="videoCentered"></video><br>
<canvas id="buffered_c" height="15" class="videoCentered" onclick="buffered_seek(event);"></canvas><br><br> <canvas id="buffered_c" height="15" class="videoCentered" onclick="buffered_seek(event);"></canvas><br><br>
<pre id="HlsStatus" class="center" style="white-space: pre-wrap;"></pre> <pre id="HlsStatus" class="center"></pre>
<div class="center" id="toggleButtons"> <div class="center" id="toggleButtons">
<button type="button" class="btn btn-sm" onclick="$('#PlaybackControl').toggle();">toggle playback controls</button> <button type="button" class="btn btn-sm" onclick="$('#PlaybackControl').toggle();">toggle playback controls</button>
@ -477,9 +477,6 @@ $(document).ready(function() {
case Hls.ErrorDetails.BUFFER_APPEND_ERROR: case Hls.ErrorDetails.BUFFER_APPEND_ERROR:
$("#HlsStatus").text("Buffer Append Error"); $("#HlsStatus").text("Buffer Append Error");
break; break;
case Hls.ErrorDetails.BUFFER_ADD_CODEC_ERROR:
$("#HlsStatus").text("Buffer Add Codec Error for " + data.mimeType + ":" + data.err.message);
break;
case Hls.ErrorDetails.BUFFER_APPENDING_ERROR: case Hls.ErrorDetails.BUFFER_APPENDING_ERROR:
$("#HlsStatus").text("Buffer Appending Error"); $("#HlsStatus").text("Buffer Appending Error");
break; break;
@ -910,10 +907,21 @@ function timeRangesToString(r) {
if(v.videoWidth) { if(v.videoWidth) {
$("#currentResolution").html("video resolution:" + v.videoWidth + 'x' + v.videoHeight); $("#currentResolution").html("video resolution:" + v.videoWidth + 'x' + v.videoHeight);
} }
$("#currentLevelControl").html(html1); if($("#currentLevelControl").html() != html1) {
$("#loadLevelControl").html(html2); $("#currentLevelControl").html(html1);
$("#levelCappingControl").html(html3); }
$("#nextLevelControl").html(html4);
if($("#loadLevelControl").html() != html2) {
$("#loadLevelControl").html(html2);
}
if($("#levelCappingControl").html() != html3) {
$("#levelCappingControl").html(html3);
}
if($("#nextLevelControl").html() != html4) {
$("#nextLevelControl").html(html4);
}
} }
function level2label(index) { function level2label(index) {

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
#/bin/sh #/bin/sh
git checkout gh-pages git checkout gh-pages
git rebase master git rebase v0.5.x
git push origin gh-pages --force git push origin gh-pages --force
git checkout master git checkout v0.5.x

View File

@ -1,6 +1,6 @@
{ {
"name": "hls.js", "name": "hls.js",
"version": "0.6.1", "version": "0.5.24",
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "Media Source Extension - HLS library, by/for Dailymotion", "description": "Media Source Extension - HLS library, by/for Dailymotion",
"homepage": "https://github.com/dailymotion/hls.js", "homepage": "https://github.com/dailymotion/hls.js",
@ -17,7 +17,7 @@
"scripts": { "scripts": {
"clean": "find dist -mindepth 1 -delete", "clean": "find dist -mindepth 1 -delete",
"prebuild": "npm run clean & npm run test", "prebuild": "npm run clean & npm run test",
"build": "npm run babel && browserify -t browserify-versionify -t [babelify] -s Hls src/index.js --debug | exorcist dist/hls.js.map -b . > dist/hls.js", "build": "npm run babel && browserify -t [babelify] -s Hls src/index.js --debug | exorcist dist/hls.js.map -b . > dist/hls.js",
"postbuild": "npm run minify", "postbuild": "npm run minify",
"prerelease": "npm run prebuild && npm run build && npm run postbuild && git add dist/* && git commit -m 'update dist'", "prerelease": "npm run prebuild && npm run build && npm run postbuild && git add dist/* && git commit -m 'update dist'",
"patch": "npm run prerelease && mversion p", "patch": "npm run prerelease && mversion p",
@ -38,14 +38,13 @@
"webworkify": "^1.0.2" "webworkify": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"arraybuffer-equal": "^1.0.4",
"babel": "^6.3.26",
"babel-cli": "^6.3.17",
"babel-preset-es2015": "^6.3.13", "babel-preset-es2015": "^6.3.13",
"babel-register": "^6.3.13", "babel-register": "^6.3.13",
"babelify": "^7.2.0", "babelify": "^7.2.0",
"arraybuffer-equal": "^1.0.4",
"babel": "^6.3.26",
"babel-cli": "^6.3.17",
"browserify": "^13.0.0", "browserify": "^13.0.0",
"browserify-versionify": "^1.0.6",
"deep-strict-equal": "^0.1.0", "deep-strict-equal": "^0.1.0",
"exorcist": "^0.4.0", "exorcist": "^0.4.0",
"http-server": "^0.9.0", "http-server": "^0.9.0",

View File

@ -67,7 +67,7 @@ class BufferController extends EventHandler {
this.mediaSource = null; this.mediaSource = null;
this.media = null; this.media = null;
this.pendingTracks = null; this.pendingTracks = null;
this.sourceBuffer = {}; this.sourceBuffer = null;
} }
this.onmso = this.onmse = this.onmsc = null; this.onmso = this.onmse = this.onmsc = null;
this.hls.trigger(Event.MEDIA_DETACHED); this.hls.trigger(Event.MEDIA_DETACHED);
@ -122,16 +122,18 @@ class BufferController extends EventHandler {
onBufferReset() { onBufferReset() {
var sourceBuffer = this.sourceBuffer; var sourceBuffer = this.sourceBuffer;
for(var type in sourceBuffer) { if (sourceBuffer) {
var sb = sourceBuffer[type]; for(var type in sourceBuffer) {
try { var sb = sourceBuffer[type];
this.mediaSource.removeSourceBuffer(sb); try {
sb.removeEventListener('updateend', this.onsbue); this.mediaSource.removeSourceBuffer(sb);
sb.removeEventListener('error', this.onsbe); sb.removeEventListener('updateend', this.onsbue);
} catch(err) { sb.removeEventListener('error', this.onsbe);
} catch(err) {
}
} }
this.sourceBuffer = null;
} }
this.sourceBuffer = {};
this.flushRange = []; this.flushRange = [];
this.appended = 0; this.appended = 0;
} }
@ -144,24 +146,19 @@ class BufferController extends EventHandler {
return; return;
} }
var sourceBuffer = this.sourceBuffer,mediaSource = this.mediaSource; if (!this.sourceBuffer) {
var sourceBuffer = {}, mediaSource = this.mediaSource;
for (trackName in tracks) { for (trackName in tracks) {
if(!sourceBuffer[trackName]) {
track = tracks[trackName]; track = tracks[trackName];
// use levelCodec as first priority // use levelCodec as first priority
codec = track.levelCodec || track.codec; codec = track.levelCodec || track.codec;
mimeType = `${track.container};codecs=${codec}`; mimeType = `${track.container};codecs=${codec}`;
logger.log(`creating sourceBuffer with mimeType:${mimeType}`); logger.log(`creating sourceBuffer with mimeType:${mimeType}`);
try { sb = sourceBuffer[trackName] = mediaSource.addSourceBuffer(mimeType);
sb = sourceBuffer[trackName] = mediaSource.addSourceBuffer(mimeType); sb.addEventListener('updateend', this.onsbue);
sb.addEventListener('updateend', this.onsbue); sb.addEventListener('error', this.onsbe);
sb.addEventListener('error', this.onsbe);
} catch(err) {
logger.error(`error while trying to add sourceBuffer:${err.message}`);
this.hls.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.BUFFER_ADD_CODEC_ERROR, fatal: false, err: err, mimeType : mimeType});
}
} }
this.sourceBuffer = sourceBuffer;
} }
} }
@ -226,8 +223,10 @@ class BufferController extends EventHandler {
// let's recompute this.appended, which is used to avoid flush looping // let's recompute this.appended, which is used to avoid flush looping
var appended = 0; var appended = 0;
var sourceBuffer = this.sourceBuffer; var sourceBuffer = this.sourceBuffer;
for (var type in sourceBuffer) { if (sourceBuffer) {
appended += sourceBuffer[type].buffered.length; for (var type in sourceBuffer) {
appended += sourceBuffer[type].buffered.length;
}
} }
this.appended = appended; this.appended = appended;
this.hls.trigger(Event.BUFFER_FLUSHED); this.hls.trigger(Event.BUFFER_FLUSHED);
@ -252,16 +251,9 @@ class BufferController extends EventHandler {
var segment = segments.shift(); var segment = segments.shift();
try { try {
//logger.log(`appending ${segment.type} SB, size:${segment.data.length}); //logger.log(`appending ${segment.type} SB, size:${segment.data.length});
if(sourceBuffer[segment.type]) { sourceBuffer[segment.type].appendBuffer(segment.data);
sourceBuffer[segment.type].appendBuffer(segment.data); this.appendError = 0;
this.appendError = 0; this.appended++;
this.appended++;
} else {
// in case we don't have any source buffer matching with this segment type,
// it means that Mediasource fails to create sourcebuffer
// discard this segment, and trigger update end
this.onSBUpdateEnd();
}
} catch(err) { } catch(err) {
// in case any error occured while appending, put back segment in segments table // in case any error occured while appending, put back segment in segments table
logger.error(`error while trying to append buffer:${err.message}`); logger.error(`error while trying to append buffer:${err.message}`);

View File

@ -8,30 +8,20 @@ import EventHandler from '../event-handler';
class CapLevelController extends EventHandler { class CapLevelController extends EventHandler {
constructor(hls) { constructor(hls) {
super(hls, super(hls,
Event.FPS_DROP_LEVEL_CAPPING,
Event.MEDIA_ATTACHING, Event.MEDIA_ATTACHING,
Event.MANIFEST_PARSED); Event.MANIFEST_PARSED);
} }
destroy() { destroy() {
if (this.hls.config.capLevelToPlayerSize) { if (this.hls.config.capLevelToPlayerSize) {
this.media = this.restrictedLevels = null; this.media = null;
this.autoLevelCapping = Number.POSITIVE_INFINITY; this.autoLevelCapping = Number.POSITIVE_INFINITY;
if (this.timer) { if (this.timer) {
this.timer = clearInterval(this.timer); this.timer = clearInterval(this.timer);
} }
} }
} }
onFpsDropLevelCapping(data) {
if (!this.restrictedLevels) {
this.restrictedLevels = [];
}
if (!this.isLevelRestricted(data.droppedLevel)) {
this.restrictedLevels.push(data.droppedLevel);
}
}
onMediaAttaching(data) { onMediaAttaching(data) {
this.media = data.media instanceof HTMLVideoElement ? data.media : null; this.media = data.media instanceof HTMLVideoElement ? data.media : null;
} }
@ -66,7 +56,7 @@ class CapLevelController extends EventHandler {
* returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled) * returns level should be the one with the dimensions equal or greater than the media (player) dimensions (so the video will be downscaled)
*/ */
getMaxLevel(capLevelIndex) { getMaxLevel(capLevelIndex) {
let result = 0, let result,
i, i,
level, level,
mWidth = this.mediaWidth, mWidth = this.mediaWidth,
@ -76,9 +66,6 @@ class CapLevelController extends EventHandler {
for (i = 0; i <= capLevelIndex; i++) { for (i = 0; i <= capLevelIndex; i++) {
level = this.levels[i]; level = this.levels[i];
if (this.isLevelRestricted(i)) {
break;
}
result = i; result = i;
lWidth = level.width; lWidth = level.width;
lHeight = level.height; lHeight = level.height;
@ -89,10 +76,6 @@ class CapLevelController extends EventHandler {
return result; return result;
} }
isLevelRestricted(level) {
return (this.restrictedLevels && this.restrictedLevels.indexOf(level) !== -1) ? true : false;
}
get contentScaleFactor() { get contentScaleFactor() {
let pixelRatio = 1; let pixelRatio = 1;
try { try {

View File

@ -3,72 +3,46 @@
*/ */
import Event from '../events'; import Event from '../events';
import EventHandler from '../event-handler';
import {logger} from '../utils/logger'; import {logger} from '../utils/logger';
class FPSController extends EventHandler{ class FPSController {
constructor(hls) { constructor(hls) {
super(hls, Event.MEDIA_ATTACHING); this.hls = hls;
this.timer = setInterval(this.checkFPS, hls.config.fpsDroppedMonitoringPeriod);
} }
destroy() { destroy() {
if (this.timer) { if (this.timer) {
clearInterval(this.timer); clearInterval(this.timer);
} }
this.isVideoPlaybackQualityAvailable = false;
} }
onMediaAttaching(data) { checkFPS() {
if (this.hls.config.capLevelOnFPSDrop) { var v = this.hls.video;
this.video = data.media instanceof HTMLVideoElement ? data.media : null; if (v) {
if (typeof this.video.getVideoPlaybackQuality === 'function') { var decodedFrames = v.webkitDecodedFrameCount, droppedFrames = v.webkitDroppedFrameCount, currentTime = new Date();
this.isVideoPlaybackQualityAvailable = true; if (decodedFrames) {
} if (this.lastTime) {
clearInterval(this.timer); var currentPeriod = currentTime - this.lastTime;
this.timer = setInterval(this.checkFPSInterval.bind(this), this.hls.config.fpsDroppedMonitoringPeriod); var currentDropped = droppedFrames - this.lastDroppedFrames;
} var currentDecoded = decodedFrames - this.lastDecodedFrames;
} var decodedFPS = 1000 * currentDecoded / currentPeriod;
var droppedFPS = 1000 * currentDropped / currentPeriod;
checkFPS(video, decodedFrames, droppedFrames) { if (droppedFPS > 0) {
let currentTime = performance.now(); logger.log(`checkFPS : droppedFPS/decodedFPS:${droppedFPS.toFixed(1)}/${decodedFPS.toFixed(1)}`);
if (decodedFrames) { if (currentDropped > this.hls.config.fpsDroppedMonitoringThreshold * currentDecoded) {
if (this.lastTime) { logger.warn('drop FPS ratio greater than max allowed value');
let currentPeriod = currentTime - this.lastTime, this.hls.trigger(Event.FPS_DROP, {currentDropped: currentDropped, currentDecoded: currentDecoded, totalDroppedFrames: droppedFrames});
currentDropped = droppedFrames - this.lastDroppedFrames,
currentDecoded = decodedFrames - this.lastDecodedFrames,
droppedFPS = 1000 * currentDropped / currentPeriod;
this.hls.trigger(Event.FPS_DROP, {currentDropped: currentDropped, currentDecoded: currentDecoded, totalDroppedFrames: droppedFrames});
if (droppedFPS > 0) {
//logger.log('checkFPS : droppedFPS/decodedFPS:' + droppedFPS/(1000 * currentDecoded / currentPeriod));
if (currentDropped > this.hls.config.fpsDroppedMonitoringThreshold * currentDecoded) {
let currentLevel = this.hls.currentLevel;
logger.warn('drop FPS ratio greater than max allowed value for currentLevel: ' + currentLevel);
if (currentLevel > 0 && (this.hls.autoLevelCapping === -1 || this.hls.autoLevelCapping >= currentLevel)) {
currentLevel = currentLevel - 1;
this.hls.trigger(Event.FPS_DROP_LEVEL_CAPPING, {level: currentLevel, droppedLevel: this.hls.currentLevel});
this.hls.autoLevelCapping = currentLevel;
this.hls.streamController.nextLevelSwitch();
} }
} }
} }
this.lastTime = currentTime;
this.lastDroppedFrames = droppedFrames;
this.lastDecodedFrames = decodedFrames;
} }
this.lastTime = currentTime;
this.lastDroppedFrames = droppedFrames;
this.lastDecodedFrames = decodedFrames;
} }
} }
checkFPSInterval() {
if (this.video) {
if (this.isVideoPlaybackQualityAvailable) {
let videoPlaybackQuality = this.video.getVideoPlaybackQuality();
this.checkFPS(this.video, videoPlaybackQuality.totalVideoFrames, videoPlaybackQuality.droppedVideoFrames);
} else {
this.checkFPS(this.video, this.video.webkitDecodedFrameCount, this.video.webkitDroppedFrameCount);
}
}
}
} }
export default FPSController; export default FPSController;

View File

@ -113,7 +113,8 @@ class LevelController extends EventHandler {
} }
set level(newLevel) { set level(newLevel) {
if (this._level !== newLevel || this._levels[newLevel].details === undefined) { if (this._levels && this._levels.length > newLevel &&
(this._level !== newLevel || this._levels[newLevel].details === undefined)) {
this.setLevelInternal(newLevel); this.setLevelInternal(newLevel);
} }
} }
@ -234,15 +235,10 @@ class LevelController extends EventHandler {
onLevelLoaded(data) { onLevelLoaded(data) {
// check if current playlist is a live playlist // check if current playlist is a live playlist
if (data.details.live) { if (data.details.live && !this.timer) {
// if live playlist we will have to reload it periodically // if live playlist we will have to reload it periodically
// set reload period to average of the frag duration, if average not set then use playlist target duration // set reload period to playlist target duration
let timerInterval = data.details.averagetargetduration ? data.details.averagetargetduration : data.details.targetduration; this.timer = setInterval(this.ontick, 1000 * data.details.targetduration);
if (!this.timer || timerInterval !== this.timerInterval) {
clearInterval(this.timer);
this.timer = setInterval(this.ontick, 1000 * timerInterval);
this.timerInterval = timerInterval;
}
} }
if (!data.details.live && this.timer) { if (!data.details.live && this.timer) {
// playlist is not live and timer is armed : stopping it // playlist is not live and timer is armed : stopping it

View File

@ -218,7 +218,7 @@ class StreamController extends EventHandler {
// level 1 loaded [182580162,182580168] <============= here we should have bufferEnd > end. in that case break to avoid reloading 182580168 // level 1 loaded [182580162,182580168] <============= here we should have bufferEnd > end. in that case break to avoid reloading 182580168
// level 1 loaded [182580164,182580171] // level 1 loaded [182580164,182580171]
// //
if (bufferEnd > end) { if (levelDetails.PTSKnown && bufferEnd > end) {
break; break;
} }
@ -486,13 +486,11 @@ class StreamController extends EventHandler {
fragCurrent.loader.abort(); fragCurrent.loader.abort();
} }
this.fragCurrent = null; this.fragCurrent = null;
// flush everything
this.hls.trigger(Event.BUFFER_FLUSHING, {startOffset: 0, endOffset: Number.POSITIVE_INFINITY});
this.state = State.PAUSED;
// increase fragment load Index to avoid frag loop loading error after buffer flush // increase fragment load Index to avoid frag loop loading error after buffer flush
this.fragLoadIdx += 2 * this.config.fragLoadingLoopThreshold; this.fragLoadIdx += 2 * this.config.fragLoadingLoopThreshold;
// speed up switching, trigger timer function this.state = State.PAUSED;
this.tick(); // flush everything
this.hls.trigger(Event.BUFFER_FLUSHING, {startOffset: 0, endOffset: Number.POSITIVE_INFINITY});
} }
/* /*
@ -515,12 +513,14 @@ class StreamController extends EventHandler {
we should take into account new segment fetch time we should take into account new segment fetch time
*/ */
var fetchdelay, currentRange, nextRange; var fetchdelay, currentRange, nextRange;
// increase fragment load Index to avoid frag loop loading error after buffer flush
this.fragLoadIdx += 2 * this.config.fragLoadingLoopThreshold;
currentRange = this.getBufferRange(this.media.currentTime); currentRange = this.getBufferRange(this.media.currentTime);
if (currentRange && currentRange.start > 1) { if (currentRange && currentRange.start > 1) {
// flush buffer preceding current fragment (flush until current fragment start offset) // flush buffer preceding current fragment (flush until current fragment start offset)
// minus 1s to avoid video freezing, that could happen if we flush keyframe of current video ... // minus 1s to avoid video freezing, that could happen if we flush keyframe of current video ...
this.hls.trigger(Event.BUFFER_FLUSHING, {startOffset: 0, endOffset: currentRange.start - 1});
this.state = State.PAUSED; this.state = State.PAUSED;
this.hls.trigger(Event.BUFFER_FLUSHING, {startOffset: 0, endOffset: currentRange.start - 1});
} }
if (!this.media.paused) { if (!this.media.paused) {
// add a safety delay of 1s // add a safety delay of 1s
@ -540,17 +540,15 @@ class StreamController extends EventHandler {
// we can flush buffer range following this one without stalling playback // we can flush buffer range following this one without stalling playback
nextRange = this.followingBufferRange(nextRange); nextRange = this.followingBufferRange(nextRange);
if (nextRange) { if (nextRange) {
// flush position is the start position of this new buffer
this.hls.trigger(Event.BUFFER_FLUSHING, {startOffset: nextRange.start, endOffset: Number.POSITIVE_INFINITY});
this.state = State.PAUSED;
// if we are here, we can also cancel any loading/demuxing in progress, as they are useless // if we are here, we can also cancel any loading/demuxing in progress, as they are useless
var fragCurrent = this.fragCurrent; var fragCurrent = this.fragCurrent;
if (fragCurrent && fragCurrent.loader) { if (fragCurrent && fragCurrent.loader) {
fragCurrent.loader.abort(); fragCurrent.loader.abort();
} }
this.fragCurrent = null; this.fragCurrent = null;
// increase fragment load Index to avoid frag loop loading error after buffer flush // flush position is the start position of this new buffer
this.fragLoadIdx += 2 * this.config.fragLoadingLoopThreshold; this.state = State.PAUSED;
this.hls.trigger(Event.BUFFER_FLUSHING, {startOffset: nextRange.start, endOffset: Number.POSITIVE_INFINITY});
} }
} }
} }
@ -1020,6 +1018,7 @@ _checkBuffer() {
// check buffer upfront // check buffer upfront
// if less than jumpThreshold second is buffered, and media is expected to play but playhead is not moving, // if less than jumpThreshold second is buffered, and media is expected to play but playhead is not moving,
// and we have a new buffer range available upfront, let's seek to that one // and we have a new buffer range available upfront, let's seek to that one
let configSeekHoleNudgeDuration = this.config.seekHoleNudgeDuration;
if(expectedPlaying && bufferInfo.len <= jumpThreshold) { if(expectedPlaying && bufferInfo.len <= jumpThreshold) {
if(playheadMoving) { if(playheadMoving) {
// playhead moving // playhead moving
@ -1033,7 +1032,7 @@ _checkBuffer() {
this.hls.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.BUFFER_STALLED_ERROR, fatal: false}); this.hls.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.BUFFER_STALLED_ERROR, fatal: false});
this.stalled = true; this.stalled = true;
} else { } else {
this.seekHoleNudgeDuration += this.config.seekHoleNudgeDuration; this.seekHoleNudgeDuration += configSeekHoleNudgeDuration;
} }
} }
// if we are below threshold, try to jump if next buffer range is close // if we are below threshold, try to jump if next buffer range is close
@ -1047,14 +1046,35 @@ _checkBuffer() {
// next buffer is close ! adjust currentTime to nextBufferStart // next buffer is close ! adjust currentTime to nextBufferStart
// this will ensure effective video decoding // this will ensure effective video decoding
logger.log(`adjust currentTime from ${media.currentTime} to next buffered @ ${nextBufferStart} + nudge ${this.seekHoleNudgeDuration}`); logger.log(`adjust currentTime from ${media.currentTime} to next buffered @ ${nextBufferStart} + nudge ${this.seekHoleNudgeDuration}`);
let hole = nextBufferStart + this.seekHoleNudgeDuration - media.currentTime;
media.currentTime = nextBufferStart + this.seekHoleNudgeDuration; media.currentTime = nextBufferStart + this.seekHoleNudgeDuration;
this.hls.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.BUFFER_SEEK_OVER_HOLE, fatal: false}); this.hls.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.BUFFER_SEEK_OVER_HOLE, fatal: false, hole : hole});
} }
} }
// in any case reset stalledInBuffered
this.stalledInBuffered = 0;
} else { } else {
if (targetSeekPosition && media.currentTime !== targetSeekPosition) { if (targetSeekPosition && media.currentTime !== targetSeekPosition) {
logger.log(`adjust currentTime from ${media.currentTime} to ${targetSeekPosition}`); logger.log(`adjust currentTime from ${media.currentTime} to ${targetSeekPosition}`);
media.currentTime = targetSeekPosition; media.currentTime = targetSeekPosition;
} else if (expectedPlaying && !playheadMoving) {
// if we are in this condition, it means that currentTime is in a buffered area, but playhead is not moving
// if that happens, we wait for a couple of cycle (config.stalledInBufferedNudgeThreshold), then we nudge
// media.currentTime to try to recover that situation.
if (this.stalledInBuffered !== undefined) {
this.stalledInBuffered++;
} else {
this.stalledInBuffered = 1;
}
if (this.stalledInBuffered >= this.config.stalledInBufferedNudgeThreshold) {
logger.log(`playback stuck @ ${media.currentTime}, in buffered area, nudge currentTime by ${configSeekHoleNudgeDuration}`);
this.hls.trigger(Event.ERROR, {type: ErrorTypes.MEDIA_ERROR, details: ErrorDetails.BUFFER_SEEK_STUCK_IN_BUFFERED, fatal: false});
media.currentTime+=configSeekHoleNudgeDuration;
this.stalledInBuffered = 0;
}
} else {
// currentTime is buffered, playhead is moving or playback not expected... everything is fine
this.stalledInBuffered = 0;
} }
} }
} }

View File

@ -36,8 +36,6 @@ export const ErrorDetails = {
KEY_LOAD_ERROR: 'keyLoadError', KEY_LOAD_ERROR: 'keyLoadError',
// Identifier for decrypt key load timeout error - data: { frag : fragment object} // Identifier for decrypt key load timeout error - data: { frag : fragment object}
KEY_LOAD_TIMEOUT: 'keyLoadTimeOut', KEY_LOAD_TIMEOUT: 'keyLoadTimeOut',
// Triggered when an exception occurs while adding a sourceBuffer to MediaSource - data : { err : exception , mimeType : mimeType }
BUFFER_ADD_CODEC_ERROR: 'bufferAddCodecError',
// Identifier for a buffer append error - data: append error description // Identifier for a buffer append error - data: append error description
BUFFER_APPEND_ERROR: 'bufferAppendError', BUFFER_APPEND_ERROR: 'bufferAppendError',
// Identifier for a buffer appending error event - data: appending error description // Identifier for a buffer appending error event - data: appending error description
@ -48,6 +46,8 @@ export const ErrorDetails = {
BUFFER_FULL_ERROR: 'bufferFullError', BUFFER_FULL_ERROR: 'bufferFullError',
// Identifier for a buffer seek over hole event // Identifier for a buffer seek over hole event
BUFFER_SEEK_OVER_HOLE: 'bufferSeekOverHole', BUFFER_SEEK_OVER_HOLE: 'bufferSeekOverHole',
// Identifier for a seek triggered to workaround a playback stuck although currentTime is buffered
BUFFER_SEEK_STUCK_IN_BUFFERED : 'bufferSeekStuckInBuffered',
// Identifier for an internal exception happening inside hls.js while handling an event // Identifier for an internal exception happening inside hls.js while handling an event
INTERNAL_EXCEPTION: 'internalException' INTERNAL_EXCEPTION: 'internalException'
}; };

View File

@ -6,6 +6,7 @@
import {logger} from './utils/logger'; import {logger} from './utils/logger';
import {ErrorTypes, ErrorDetails} from './errors'; import {ErrorTypes, ErrorDetails} from './errors';
import Event from './events';
class EventHandler { class EventHandler {

View File

@ -59,10 +59,8 @@ module.exports = {
FRAG_BUFFERED: 'hlsFragBuffered', FRAG_BUFFERED: 'hlsFragBuffered',
// fired when fragment matching with current media position is changing - data : { frag : fragment object } // fired when fragment matching with current media position is changing - data : { frag : fragment object }
FRAG_CHANGED: 'hlsFragChanged', FRAG_CHANGED: 'hlsFragChanged',
// Identifier for a FPS drop event - data: {curentDropped, currentDecoded, totalDroppedFrames} // Identifier for a FPS drop event - data: {curentDropped, currentDecoded, totalDroppedFrames}
FPS_DROP: 'hlsFpsDrop', FPS_DROP: 'hlsFpsDrop',
//triggered when FPS drop triggers auto level capping - data: {level, droppedlevel}
FPS_DROP_LEVEL_CAPPING: 'hlsFpsDropLevelCapping',
// Identifier for an error event - data: { type : error type, details : error details, fatal : if true, hls.js cannot/will not try to recover, if false, hls.js will try to recover,other error specific data} // Identifier for an error event - data: { type : error type, details : error details, fatal : if true, hls.js cannot/will not try to recover, if false, hls.js will try to recover,other error specific data}
ERROR: 'hlsError', ERROR: 'hlsError',
// fired when hls.js instance starts destroying. Different from MEDIA_DETACHED as one could want to detach and reattach a media to the instance of hls.js to handle mid-rolls for example // fired when hls.js instance starts destroying. Different from MEDIA_DETACHED as one could want to detach and reattach a media to the instance of hls.js to handle mid-rolls for example

View File

@ -13,7 +13,7 @@ import CapLevelController from './controller/cap-level-controller';
import StreamController from './controller/stream-controller'; import StreamController from './controller/stream-controller';
import LevelController from './controller/level-controller'; import LevelController from './controller/level-controller';
import TimelineController from './controller/timeline-controller'; import TimelineController from './controller/timeline-controller';
import FPSController from './controller/fps-controller'; //import FPSController from './controller/fps-controller';
import {logger, enableLogs} from './utils/logger'; import {logger, enableLogs} from './utils/logger';
import XhrLoader from './utils/xhr-loader'; import XhrLoader from './utils/xhr-loader';
import EventEmitter from 'events'; import EventEmitter from 'events';
@ -21,11 +21,6 @@ import KeyLoader from './loader/key-loader';
class Hls { class Hls {
static get version() {
// replaced with browserify-versionify transform
return '__VERSION__';
}
static isSupported() { static isSupported() {
return (window.MediaSource && window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"')); return (window.MediaSource && window.MediaSource.isTypeSupported('video/mp4; codecs="avc1.42E01E,mp4a.40.2"'));
} }
@ -47,13 +42,13 @@ class Hls {
Hls.defaultConfig = { Hls.defaultConfig = {
autoStartLoad: true, autoStartLoad: true,
debug: false, debug: false,
capLevelOnFPSDrop: false,
capLevelToPlayerSize: false, capLevelToPlayerSize: false,
maxBufferLength: 30, maxBufferLength: 30,
maxBufferSize: 60 * 1000 * 1000, maxBufferSize: 60 * 1000 * 1000,
maxBufferHole: 0.5, maxBufferHole: 0.5,
maxSeekHole: 2, maxSeekHole: 2,
seekHoleNudgeDuration : 0.01, seekHoleNudgeDuration : 0.01,
stalledInBufferedNudgeThreshold: 10,
maxFragLookUpTolerance : 0.2, maxFragLookUpTolerance : 0.2,
liveSyncDurationCount:3, liveSyncDurationCount:3,
liveMaxLatencyDurationCount: Infinity, liveMaxLatencyDurationCount: Infinity,
@ -73,8 +68,8 @@ class Hls {
fragLoadingRetryDelay: 1000, fragLoadingRetryDelay: 1000,
fragLoadingLoopThreshold: 3, fragLoadingLoopThreshold: 3,
startFragPrefetch : false, startFragPrefetch : false,
fpsDroppedMonitoringPeriod: 5000, // fpsDroppedMonitoringPeriod: 5000,
fpsDroppedMonitoringThreshold: 0.2, // fpsDroppedMonitoringThreshold: 0.2,
appendErrorMaxRetry: 3, appendErrorMaxRetry: 3,
loader: XhrLoader, loader: XhrLoader,
fLoader: undefined, fLoader: undefined,
@ -82,7 +77,6 @@ class Hls {
abrController : AbrController, abrController : AbrController,
bufferController : BufferController, bufferController : BufferController,
capLevelController : CapLevelController, capLevelController : CapLevelController,
fpsController: FPSController,
streamController: StreamController, streamController: StreamController,
timelineController: TimelineController, timelineController: TimelineController,
enableCEA708Captions: true, enableCEA708Captions: true,
@ -136,10 +130,10 @@ class Hls {
this.abrController = new config.abrController(this); this.abrController = new config.abrController(this);
this.bufferController = new config.bufferController(this); this.bufferController = new config.bufferController(this);
this.capLevelController = new config.capLevelController(this); this.capLevelController = new config.capLevelController(this);
this.fpsController = new config.fpsController(this);
this.streamController = new config.streamController(this); this.streamController = new config.streamController(this);
this.timelineController = new config.timelineController(this); this.timelineController = new config.timelineController(this);
this.keyLoader = new KeyLoader(this); this.keyLoader = new KeyLoader(this);
//this.fpsController = new FPSController(this);
} }
destroy() { destroy() {
@ -152,10 +146,10 @@ class Hls {
this.abrController.destroy(); this.abrController.destroy();
this.bufferController.destroy(); this.bufferController.destroy();
this.capLevelController.destroy(); this.capLevelController.destroy();
this.fpsController.destroy();
this.streamController.destroy(); this.streamController.destroy();
this.timelineController.destroy(); this.timelineController.destroy();
this.keyLoader.destroy(); this.keyLoader.destroy();
//this.fpsController.destroy();
this.url = null; this.url = null;
this.observer.removeAllListeners(); this.observer.removeAllListeners();
} }

View File

@ -51,7 +51,7 @@ class FragmentLoader extends EventHandler {
this.hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.FRAG_LOAD_TIMEOUT, fatal: false, frag: this.frag}); this.hls.trigger(Event.ERROR, {type: ErrorTypes.NETWORK_ERROR, details: ErrorDetails.FRAG_LOAD_TIMEOUT, fatal: false, frag: this.frag});
} }
loadprogress(stats) { loadprogress(event, stats) {
this.frag.loaded = stats.loaded; this.frag.loaded = stats.loaded;
this.hls.trigger(Event.FRAG_LOAD_PROGRESS, {frag: this.frag, stats: stats}); this.hls.trigger(Event.FRAG_LOAD_PROGRESS, {frag: this.frag, stats: stats});
} }

View File

@ -211,7 +211,6 @@ class PlaylistLoader extends EventHandler {
totalduration-=frag.duration; totalduration-=frag.duration;
} }
level.totalduration = totalduration; level.totalduration = totalduration;
level.averagetargetduration = totalduration / level.fragments.length;
level.endSN = currentSN - 1; level.endSN = currentSN - 1;
return level; return level;
} }
@ -225,8 +224,7 @@ class PlaylistLoader extends EventHandler {
hls = this.hls, hls = this.hls,
levels; levels;
// responseURL not supported on some browsers (it is used to detect URL redirection) // responseURL not supported on some browsers (it is used to detect URL redirection)
// data-uri mode also not supported (but no need to detect redirection) if (url === undefined) {
if (url === undefined || url.indexOf('data:') === 0) {
// fallback to initial URL // fallback to initial URL
url = this.url; url = this.url;
} }

View File

@ -132,164 +132,135 @@ class MP4Remuxer {
} }
remuxVideo(track, timeOffset, contiguous) { remuxVideo(track, timeOffset, contiguous) {
var offset = 8, var view,
offset = 8,
pesTimeScale = this.PES_TIMESCALE, pesTimeScale = this.PES_TIMESCALE,
pes2mp4ScaleFactor = this.PES2MP4SCALEFACTOR, pes2mp4ScaleFactor = this.PES2MP4SCALEFACTOR,
mp4SampleDuration, avcSample,
mp4Sample,
mp4SampleLength,
unit,
mdat, moof, mdat, moof,
firstPTS, firstDTS, firstPTS, firstDTS, lastDTS,
nextDTS, pts, dts, ptsnorm, dtsnorm,
lastPTS, lastDTS, flags,
inputSamples = track.samples, samples = [];
outputSamples = [];
// PTS is coded on 33bits, and can loop from -2^32 to 2^32
// PTSNormalize will make PTS/DTS value monotonic, we use last known DTS value as reference value
let nextAvcDts;
if (contiguous) {
// if parsed fragment is contiguous with last one, let's use last DTS value as reference
nextAvcDts = this.nextAvcDts;
} else {
// if not contiguous, let's use target timeOffset
nextAvcDts = timeOffset*pesTimeScale;
}
// compute first DTS and last DTS, normalize them against reference value
let sample = inputSamples[0];
firstDTS = Math.max(this._PTSNormalize(sample.dts,nextAvcDts) - this._initDTS,0);
firstPTS = Math.max(this._PTSNormalize(sample.pts,nextAvcDts) - this._initDTS,0);
// check timestamp continuity accross consecutive fragments (this is to remove inter-fragment gap/hole)
let delta = Math.round((firstDTS - nextAvcDts) / 90);
// if fragment are contiguous, or if there is a huge delta (more than 10s) between expected PTS and sample PTS
if (contiguous || Math.abs(delta) > 10000) {
if (delta) {
if (delta > 1) {
logger.log(`AVC:${delta} ms hole between fragments detected,filling it`);
} else if (delta < -1) {
logger.log(`AVC:${(-delta)} ms overlapping between fragments detected`);
}
// remove hole/gap : set DTS to next expected DTS
firstDTS = nextAvcDts;
inputSamples[0].dts = firstDTS + this._initDTS;
// offset PTS as well, ensure that PTS is smaller or equal than new DTS
firstPTS = Math.max(firstPTS - delta, nextAvcDts);
inputSamples[0].pts = firstPTS + this._initDTS;
logger.log(`Video/PTS/DTS adjusted: ${firstPTS}/${firstDTS},delta:${delta}`);
}
}
nextDTS = firstDTS;
// compute lastPTS/lastDTS
sample = inputSamples[inputSamples.length-1];
lastDTS = Math.max(this._PTSNormalize(sample.dts,nextAvcDts) - this._initDTS,0);
lastPTS = Math.max(this._PTSNormalize(sample.pts,nextAvcDts) - this._initDTS,0);
lastPTS = Math.max(lastPTS, lastDTS);
let vendor = navigator.vendor, userAgent = navigator.userAgent,
isSafari = vendor && vendor.indexOf('Apple') > -1 && userAgent && !userAgent.match('CriOS');
// on Safari let's signal the same sample duration for all samples
// sample duration (as expected by trun MP4 boxes), should be the delta between sample DTS
// set this constant duration as being the avg delta between consecutive DTS.
if (isSafari) {
mp4SampleDuration = Math.round((lastDTS-firstDTS)/(pes2mp4ScaleFactor*(inputSamples.length-1)));
}
// normalize all PTS/DTS now ...
for (let i = 0; i < inputSamples.length; i++) {
let sample = inputSamples[i];
if (isSafari) {
// sample DTS is computed using a constant decoding offset (mp4SampleDuration) between samples
sample.dts = firstDTS + i*pes2mp4ScaleFactor*mp4SampleDuration;
} else {
// ensure sample monotonic DTS
sample.dts = Math.max(this._PTSNormalize(sample.dts, nextAvcDts) - this._initDTS,firstDTS);
// ensure dts is a multiple of scale factor to avoid rounding issues
sample.dts = Math.round(sample.dts/pes2mp4ScaleFactor)*pes2mp4ScaleFactor;
}
// we normalize PTS against nextAvcDts, we also substract initDTS (some streams don't start @ PTS O)
// and we ensure that computed value is greater or equal than sample DTS
sample.pts = Math.max(this._PTSNormalize(sample.pts,nextAvcDts) - this._initDTS, sample.dts);
// ensure pts is a multiple of scale factor to avoid rounding issues
sample.pts = Math.round(sample.pts/pes2mp4ScaleFactor)*pes2mp4ScaleFactor;
}
/* concatenate the video data and construct the mdat in place /* concatenate the video data and construct the mdat in place
(need 8 more bytes to fill length and mpdat type) */ (need 8 more bytes to fill length and mpdat type) */
mdat = new Uint8Array(track.len + (4 * track.nbNalu) + 8); mdat = new Uint8Array(track.len + (4 * track.nbNalu) + 8);
let view = new DataView(mdat.buffer); view = new DataView(mdat.buffer);
view.setUint32(0, mdat.byteLength); view.setUint32(0, mdat.byteLength);
mdat.set(MP4.types.mdat, 4); mdat.set(MP4.types.mdat, 4);
while (track.samples.length) {
for (let i = 0; i < inputSamples.length; i++) { avcSample = track.samples.shift();
let avcSample = inputSamples[i], mp4SampleLength = 0;
mp4SampleLength = 0,
compositionTimeOffset;
// convert NALU bitstream to MP4 format (prepend NALU with size field) // convert NALU bitstream to MP4 format (prepend NALU with size field)
while (avcSample.units.units.length) { while (avcSample.units.units.length) {
let unit = avcSample.units.units.shift(); unit = avcSample.units.units.shift();
view.setUint32(offset, unit.data.byteLength); view.setUint32(offset, unit.data.byteLength);
offset += 4; offset += 4;
mdat.set(unit.data, offset); mdat.set(unit.data, offset);
offset += unit.data.byteLength; offset += unit.data.byteLength;
mp4SampleLength += 4 + unit.data.byteLength; mp4SampleLength += 4 + unit.data.byteLength;
} }
pts = avcSample.pts - this._initDTS;
if(!isSafari) { dts = avcSample.dts - this._initDTS;
// expected sample duration is the Decoding Timestamp diff of consecutive samples // ensure DTS is not bigger than PTS
if (i < inputSamples.length - 1) { dts = Math.min(pts,dts);
mp4SampleDuration = inputSamples[i+1].dts - avcSample.dts; //logger.log(`Video/PTS/DTS:${Math.round(pts/90)}/${Math.round(dts/90)}`);
} else { // if not first AVC sample of video track, normalize PTS/DTS with previous sample value
// last sample duration is same than previous one // and ensure that sample duration is positive
mp4SampleDuration = avcSample.dts - inputSamples[i-1].dts; if (lastDTS !== undefined) {
ptsnorm = this._PTSNormalize(pts, lastDTS);
dtsnorm = this._PTSNormalize(dts, lastDTS);
var sampleDuration = (dtsnorm - lastDTS) / pes2mp4ScaleFactor;
if (sampleDuration <= 0) {
logger.log(`invalid sample duration at PTS/DTS: ${avcSample.pts}/${avcSample.dts}:${sampleDuration}`);
sampleDuration = 1;
} }
mp4SampleDuration /= pes2mp4ScaleFactor; mp4Sample.duration = sampleDuration;
compositionTimeOffset = Math.round((avcSample.pts - avcSample.dts) / pes2mp4ScaleFactor);
} else { } else {
compositionTimeOffset = Math.max(0,mp4SampleDuration*Math.round((avcSample.pts - avcSample.dts)/(pes2mp4ScaleFactor*mp4SampleDuration))); let nextAvcDts, delta;
if (contiguous) {
nextAvcDts = this.nextAvcDts;
} else {
nextAvcDts = timeOffset*pesTimeScale;
}
// first AVC sample of video track, normalize PTS/DTS
ptsnorm = this._PTSNormalize(pts, nextAvcDts);
dtsnorm = this._PTSNormalize(dts, nextAvcDts);
delta = Math.round((dtsnorm - nextAvcDts) / 90);
// if fragment are contiguous, detect hole/overlapping between fragments
if (contiguous) {
if (delta) {
if (delta > 1) {
logger.log(`AVC:${delta} ms hole between fragments detected,filling it`);
} else if (delta < -1) {
logger.log(`AVC:${(-delta)} ms overlapping between fragments detected`);
}
// set DTS to next DTS
dtsnorm = nextAvcDts;
// offset PTS as well, ensure that PTS is smaller or equal than new DTS
ptsnorm = Math.max(ptsnorm - delta, dtsnorm);
logger.log(`Video/PTS/DTS adjusted: ${ptsnorm}/${dtsnorm},delta:${delta}`);
}
}
// remember first PTS of our avcSamples, ensure value is positive
firstPTS = Math.max(0, ptsnorm);
firstDTS = Math.max(0, dtsnorm);
} }
//console.log('PTS/DTS/initDTS/normPTS/normDTS/relative PTS : ${avcSample.pts}/${avcSample.dts}/${this._initDTS}/${ptsnorm}/${dtsnorm}/${(avcSample.pts/4294967296).toFixed(3)}'); //console.log('PTS/DTS/initDTS/normPTS/normDTS/relative PTS : ${avcSample.pts}/${avcSample.dts}/${this._initDTS}/${ptsnorm}/${dtsnorm}/${(avcSample.pts/4294967296).toFixed(3)}');
outputSamples.push({ mp4Sample = {
size: mp4SampleLength, size: mp4SampleLength,
// constant duration duration: 0,
duration: mp4SampleDuration, cts: (ptsnorm - dtsnorm) / pes2mp4ScaleFactor,
cts: compositionTimeOffset,
flags: { flags: {
isLeading: 0, isLeading: 0,
isDependedOn: 0, isDependedOn: 0,
hasRedundancy: 0, hasRedundancy: 0,
degradPrio: 0, degradPrio: 0
dependsOn : avcSample.key ? 2 : 1,
isNonSync : avcSample.key ? 0 : 1
} }
}); };
flags = mp4Sample.flags;
if (avcSample.key === true) {
// the current sample is a key frame
flags.dependsOn = 2;
flags.isNonSync = 0;
} else {
flags.dependsOn = 1;
flags.isNonSync = 1;
}
samples.push(mp4Sample);
lastDTS = dtsnorm;
} }
// next AVC sample DTS should be equal to last sample DTS + last sample duration (in PES timescale) var lastSampleDuration = 0;
this.nextAvcDts = lastDTS + mp4SampleDuration*pes2mp4ScaleFactor; if (samples.length >= 2) {
lastSampleDuration = samples[samples.length - 2].duration;
mp4Sample.duration = lastSampleDuration;
}
// next AVC sample DTS should be equal to last sample DTS + last sample duration
this.nextAvcDts = dtsnorm + lastSampleDuration * pes2mp4ScaleFactor;
track.len = 0; track.len = 0;
track.nbNalu = 0; track.nbNalu = 0;
if(outputSamples.length && navigator.userAgent.toLowerCase().indexOf('chrome') > -1) { if(samples.length && navigator.userAgent.toLowerCase().indexOf('chrome') > -1) {
let flags = outputSamples[0].flags; flags = samples[0].flags;
// chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue // chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue
// https://code.google.com/p/chromium/issues/detail?id=229412 // https://code.google.com/p/chromium/issues/detail?id=229412
flags.dependsOn = 2; flags.dependsOn = 2;
flags.isNonSync = 0; flags.isNonSync = 0;
} }
track.samples = outputSamples; track.samples = samples;
moof = MP4.moof(track.sequenceNumber++, firstDTS / pes2mp4ScaleFactor, track); moof = MP4.moof(track.sequenceNumber++, firstDTS / pes2mp4ScaleFactor, track);
track.samples = []; track.samples = [];
this.observer.trigger(Event.FRAG_PARSING_DATA, { this.observer.trigger(Event.FRAG_PARSING_DATA, {
data1: moof, data1: moof,
data2: mdat, data2: mdat,
startPTS: firstPTS / pesTimeScale, startPTS: firstPTS / pesTimeScale,
endPTS: (lastPTS + pes2mp4ScaleFactor * mp4SampleDuration) / pesTimeScale, endPTS: (ptsnorm + pes2mp4ScaleFactor * lastSampleDuration) / pesTimeScale,
startDTS: firstDTS / pesTimeScale, startDTS: firstDTS / pesTimeScale,
endDTS: this.nextAvcDts / pesTimeScale, endDTS: this.nextAvcDts / pesTimeScale,
type: 'video', type: 'video',
nb: outputSamples.length nb: samples.length
}); });
} }
@ -328,7 +299,7 @@ class MP4Remuxer {
mp4Sample.duration = (dtsnorm - lastDTS) / pes2mp4ScaleFactor; mp4Sample.duration = (dtsnorm - lastDTS) / pes2mp4ScaleFactor;
if(Math.abs(mp4Sample.duration - expectedSampleDuration) > expectedSampleDuration/10) { if(Math.abs(mp4Sample.duration - expectedSampleDuration) > expectedSampleDuration/10) {
// more than 10% diff between sample duration and expectedSampleDuration .... lets log that // more than 10% diff between sample duration and expectedSampleDuration .... lets log that
logger.trace(`invalid AAC sample duration at PTS ${Math.round(pts/90)},should be 1024,found :${Math.round(mp4Sample.duration*track.audiosamplerate/track.timescale)}`); logger.log(`invalid AAC sample duration at PTS ${Math.round(pts/90)},should be 1024,found :${Math.round(mp4Sample.duration*track.audiosamplerate/track.timescale)}`);
} }
// always adjust sample duration to avoid av sync issue // always adjust sample duration to avoid av sync issue
mp4Sample.duration = expectedSampleDuration; mp4Sample.duration = expectedSampleDuration;
@ -343,20 +314,20 @@ class MP4Remuxer {
ptsnorm = this._PTSNormalize(pts, nextAacPts); ptsnorm = this._PTSNormalize(pts, nextAacPts);
dtsnorm = this._PTSNormalize(dts, nextAacPts); dtsnorm = this._PTSNormalize(dts, nextAacPts);
delta = Math.round(1000 * (ptsnorm - nextAacPts) / pesTimeScale); delta = Math.round(1000 * (ptsnorm - nextAacPts) / pesTimeScale);
// if fragment are contiguous, or if there is a huge delta (more than 10s) between expected PTS and sample PTS // if fragment are contiguous, detect hole/overlapping between fragments
if (contiguous || Math.abs(delta) > 10000) { if (contiguous) {
// log delta // log delta
if (delta) { if (delta) {
if (delta > 0) { if (delta > 0) {
logger.log(`${delta} ms hole between AAC samples detected,filling it`); logger.log(`${delta} ms hole between AAC samples detected,filling it`);
// if we have frame overlap, overlapping for more than half a frame duraion // if we have frame overlap, overlapping for more than half a frame duration
} else if (delta < -12) { } else if (delta < -12) {
// drop overlapping audio frames... browser will deal with it // drop overlapping audio frames... browser will deal with it
logger.log(`${(-delta)} ms overlapping between AAC samples detected, drop frame`); logger.log(`${(-delta)} ms overlapping between AAC samples detected, drop frame`);
track.len -= unit.byteLength; track.len -= unit.byteLength;
continue; continue;
} }
// set PTS/DTS to expected PTS/DTS // set PTS/DTS to next PTS/DTS
ptsnorm = dtsnorm = nextAacPts; ptsnorm = dtsnorm = nextAacPts;
} }
} }

View File

@ -112,11 +112,8 @@ class XhrLoader {
stats.tfirst = performance.now(); stats.tfirst = performance.now();
} }
stats.loaded = event.loaded; stats.loaded = event.loaded;
if (event.lengthComputable) {
stats.total = event.total;
}
if (this.onProgress) { if (this.onProgress) {
this.onProgress(stats); this.onProgress(event, stats);
} }
} }
} }

View File

@ -30,14 +30,14 @@
"web-component-tester": "polymer/web-component-tester#^3.4.0" "web-component-tester": "polymer/web-component-tester#^3.4.0"
}, },
"ignore": [], "ignore": [],
"homepage": "https://github.com/PolymerElements/iron-a11y-announcer", "homepage": "https://github.com/polymerelements/iron-a11y-announcer",
"_release": "1.0.4", "_release": "1.0.4",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "v1.0.4", "tag": "v1.0.4",
"commit": "5ce3eb8c4282bb53cd72e348858dc6be6b4c50b9" "commit": "5ce3eb8c4282bb53cd72e348858dc6be6b4c50b9"
}, },
"_source": "git://github.com/PolymerElements/iron-a11y-announcer.git", "_source": "git://github.com/polymerelements/iron-a11y-announcer.git",
"_target": "^1.0.0", "_target": "^1.0.0",
"_originalSource": "PolymerElements/iron-a11y-announcer" "_originalSource": "polymerelements/iron-a11y-announcer"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "iron-behaviors", "name": "iron-behaviors",
"version": "1.0.13", "version": "1.0.14",
"description": "Provides a set of behaviors for the iron elements", "description": "Provides a set of behaviors for the iron elements",
"private": true, "private": true,
"authors": [ "authors": [
@ -29,14 +29,14 @@
"webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0" "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0"
}, },
"ignore": [], "ignore": [],
"homepage": "https://github.com/polymerelements/iron-behaviors", "homepage": "https://github.com/PolymerElements/iron-behaviors",
"_release": "1.0.13", "_release": "1.0.14",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "v1.0.13", "tag": "v1.0.14",
"commit": "a7bc3428a6da2beed21987b3a8028206826a12bc" "commit": "c1d38a26219cf2e83b31a71a2bd8ae35ebee7ed7"
}, },
"_source": "git://github.com/polymerelements/iron-behaviors.git", "_source": "git://github.com/PolymerElements/iron-behaviors.git",
"_target": "^1.0.0", "_target": "^1.0.0",
"_originalSource": "polymerelements/iron-behaviors" "_originalSource": "PolymerElements/iron-behaviors"
} }

View File

@ -0,0 +1,33 @@
<!-- Instructions: https://github.com/PolymerElements/iron-behaviors/CONTRIBUTING.md#filing-issues -->
### Description
<!-- Example: The `paper-foo` element causes the page to turn pink when clicked. -->
### Expected outcome
<!-- Example: The page stays the same color. -->
### Actual outcome
<!-- Example: The page turns pink. -->
### Live Demo
<!-- Example: https://jsbin.com/cagaye/edit?html,output -->
### Steps to reproduce
<!-- Example
1. Put a `paper-foo` element in the page.
2. Open the page in a web browser.
3. Click the `paper-foo` element.
-->
### Browsers Affected
<!-- Check all that apply -->
- [ ] Chrome
- [ ] Firefox
- [ ] Safari 9
- [ ] Safari 8
- [ ] Safari 7
- [ ] Edge
- [ ] IE 11
- [ ] IE 10

View File

@ -1,5 +1,5 @@
language: node_js language: node_js
sudo: false sudo: required
before_script: before_script:
- npm install -g bower polylint web-component-tester - npm install -g bower polylint web-component-tester
- bower install - bower install
@ -8,18 +8,16 @@ env:
global: global:
- secure: ZOqj2XVNVwfT74rHxg/ljcAsS6FnmDpRSsXbsy1Icv9DcLHrMlmyQ10gWBjE/YXYF0Uv4akQ1qqn0TJaKOtp9HZeH+P6OPAYk2vJbWD7qp52pPtIqEFomcsUyflt4IjfaXKuN4FMod7PSWVSGJ+DxSguJvZKILkrs5d/rJdFv3c= - secure: ZOqj2XVNVwfT74rHxg/ljcAsS6FnmDpRSsXbsy1Icv9DcLHrMlmyQ10gWBjE/YXYF0Uv4akQ1qqn0TJaKOtp9HZeH+P6OPAYk2vJbWD7qp52pPtIqEFomcsUyflt4IjfaXKuN4FMod7PSWVSGJ+DxSguJvZKILkrs5d/rJdFv3c=
- secure: clkqemGQG16TXyAPkv9LBv6x3SbT3ZM0eo8LETx4uNKi3WzlwgXxZA9b5Sr5wYzxyxFFpnhDXW7CL4+UjYu1atGNeTW2TuSaYUPHtgu67OFDr8Jbw047p1XQb5enPSt9+YxrHKfjHBiJvWulJ8rCSQshU9Rhe0DC6NrFRPFgk0A= - secure: clkqemGQG16TXyAPkv9LBv6x3SbT3ZM0eo8LETx4uNKi3WzlwgXxZA9b5Sr5wYzxyxFFpnhDXW7CL4+UjYu1atGNeTW2TuSaYUPHtgu67OFDr8Jbw047p1XQb5enPSt9+YxrHKfjHBiJvWulJ8rCSQshU9Rhe0DC6NrFRPFgk0A=
- CXX=g++-4.8
node_js: stable node_js: stable
addons: addons:
firefox: latest firefox: latest
apt: apt:
sources: sources:
- google-chrome - google-chrome
- ubuntu-toolchain-r-test
packages: packages:
- google-chrome-stable - google-chrome-stable
- g++-4.8
sauce_connect: true sauce_connect: true
script: script:
- xvfb-run wct - xvfb-run wct
- "if [ \"${TRAVIS_PULL_REQUEST}\" = \"false\" ]; then wct -s 'default'; fi" - "if [ \"${TRAVIS_PULL_REQUEST}\" = \"false\" ]; then wct -s 'default'; fi"
dist: trusty

View File

@ -1,6 +1,6 @@
{ {
"name": "iron-behaviors", "name": "iron-behaviors",
"version": "1.0.13", "version": "1.0.14",
"description": "Provides a set of behaviors for the iron elements", "description": "Provides a set of behaviors for the iron elements",
"private": true, "private": true,
"authors": [ "authors": [

View File

@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<!-- <!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved. Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt

View File

@ -1,4 +1,5 @@
<!-- <!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved. Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt

View File

@ -87,7 +87,7 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
this.style.pointerEvents = disabled ? 'none' : ''; this.style.pointerEvents = disabled ? 'none' : '';
if (disabled) { if (disabled) {
this._oldTabIndex = this.tabIndex; this._oldTabIndex = this.tabIndex;
this.focused = false; this._setFocused(false);
this.tabIndex = -1; this.tabIndex = -1;
this.blur(); this.blur();
} else if (this._oldTabIndex !== undefined) { } else if (this._oldTabIndex !== undefined) {

View File

@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<!-- <!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved. Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt

View File

@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<!-- <!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved. Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt

View File

@ -1,5 +1,6 @@
<!doctype html> <!doctype html>
<!-- <!--
@license
Copyright (c) 2015 The Polymer Project Authors. All rights reserved. Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt

Some files were not shown because too many files have changed in this diff Show More