reduce traffic from play to feature

This commit is contained in:
Luke Pulverenti 2017-01-24 14:54:18 -05:00
parent 7b80b2ea32
commit 58cebe2486
18 changed files with 157 additions and 59 deletions

View File

@ -16,12 +16,12 @@
},
"devDependencies": {},
"ignore": [],
"version": "1.1.121",
"_release": "1.1.121",
"version": "1.1.125",
"_release": "1.1.125",
"_resolution": {
"type": "version",
"tag": "1.1.121",
"commit": "5785022dd5fdf3633606d4c84d59f52ffa61251d"
"tag": "1.1.125",
"commit": "de914c8db94b994ea45746626ba73bfb72a594a0"
},
"_source": "https://github.com/MediaBrowser/Emby.ApiClient.Javascript.git",
"_target": "^1.1.51",

View File

@ -1408,6 +1408,26 @@
return self.getJSON(url);
};
self.getPlaybackInfo = function(itemId, options, deviceProfile) {
var postData = {
DeviceProfile: deviceProfile
};
return self.ajax({
url: self.getUrl('Items/' + itemId + '/PlaybackInfo', options),
type: 'POST',
data: JSON.stringify(postData),
contentType: "application/json",
dataType: "json"
});
};
self.getIntros = function(itemId) {
return self.getJSON(self.getUrl('Users/' + self.getCurrentUserId() + '/Items/' + itemId + '/Intros'));
};
/**
* Gets the directory contents of a path on the server
*/

View File

@ -298,7 +298,65 @@
return result;
}
function getPlaybackInfo(itemId, options, deviceProfile) {
return localassetmanager.getLocalItem(apiclientcore.serverId(), stripStart(itemId, localPrefix)).then(function (item) {
// TODO: This was already done during the sync process, right? If so, remove it
var mediaSources = item.Item.MediaSources.map(function (m) {
m.SupportsDirectPlay = true;
m.SupportsDirectStream = false;
m.SupportsTranscoding = false;
return m;
});
return {
MediaSources: mediaSources
};
});
}
// "Override" methods
self.detectBitrate = function () {
return Promise.reject();
};
self.reportPlaybackStart = function (options) {
if (!options) {
throw new Error("null options");
}
return Promise.resolve();
};
self.reportPlaybackProgress = function (options) {
if (!options) {
throw new Error("null options");
}
return Promise.resolve();
};
self.reportPlaybackStopped = function (options) {
if (!options) {
throw new Error("null options");
}
return Promise.resolve();
};
self.getIntros = function (itemId) {
return Promise.resolve({
Items: [],
TotalRecordCount: 0
});
};
self.getUserViews = getUserViews;
self.getItems = getItems;
self.getItem = getItem;
@ -309,6 +367,7 @@
self.getSimilarItems = getSimilarItems;
self.updateFavoriteStatus = updateFavoriteStatus;
self.getScaledImageUrl = getScaledImageUrl;
self.getPlaybackInfo = getPlaybackInfo;
};
});

View File

@ -14,12 +14,12 @@
},
"devDependencies": {},
"ignore": [],
"version": "1.4.484",
"_release": "1.4.484",
"version": "1.4.489",
"_release": "1.4.489",
"_resolution": {
"type": "version",
"tag": "1.4.484",
"commit": "ed56575477c1115b8583613135206ed49841c70f"
"tag": "1.4.489",
"commit": "4b7b914b55dba588627036dee142a3358cb1c282"
},
"_source": "https://github.com/MediaBrowser/emby-webcomponents.git",
"_target": "^1.2.1",

View File

@ -23,7 +23,7 @@
function forceRefresh(loading) {
var elem = document.body;
var elem = this.parentNode;
elem.style.webkitAnimationName = 'repaintChrome';
elem.style.webkitAnimationDelay = (loading === true ? '500ms' : '');
@ -66,7 +66,7 @@
if (enableRefreshHack) {
forceRefresh(true);
forceRefresh.call(this, true);
dom.addEventListener(this, 'click', forceRefresh, {
passive: true
});

View File

@ -1519,7 +1519,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g
var apiClient = connectionManager.getApiClient(firstItem.ServerId);
return apiClient.getJSON(apiClient.getUrl('Users/' + apiClient.getCurrentUserId() + '/Items/' + firstItem.Id + '/Intros')).then(function (intros) {
return apiClient.getIntros(firstItem.Id).then(function (intros) {
items = intros.Items.concat(items);
currentPlayOptions = options;
@ -1895,6 +1895,11 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g
playMethod = 'DirectPlay';
}
// Fallback (used for offline items)
if (!mediaUrl && mediaSource.SupportsDirectPlay) {
mediaUrl = mediaSource.Path;
}
var resultInfo = {
url: mediaUrl,
mimeType: contentType,
@ -2013,10 +2018,6 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g
function getPlaybackInfo(apiClient, itemId, deviceProfile, maxBitrate, startPosition, mediaSource, audioStreamIndex, subtitleStreamIndex, liveStreamId) {
var postData = {
DeviceProfile: deviceProfile
};
var query = {
UserId: apiClient.getCurrentUserId(),
StartTimeTicks: startPosition || 0
@ -2038,14 +2039,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g
query.MaxStreamingBitrate = maxBitrate;
}
return apiClient.ajax({
url: apiClient.getUrl('Items/' + itemId + '/PlaybackInfo', query),
type: 'POST',
data: JSON.stringify(postData),
contentType: "application/json",
dataType: "json"
});
return apiClient.getPlaybackInfo(itemId, query, deviceProfile);
}
function getOptimalMediaSource(apiClient, item, versions) {
@ -2081,7 +2075,7 @@ define(['events', 'datetime', 'appSettings', 'pluginManager', 'userSettings', 'g
return s.SupportsTranscoding;
})[0];
return optimalVersion;
return optimalVersion || versions[0];
});
}

View File

@ -1,8 +1,8 @@
{
"HeaderRemoteControl": "Remote Control",
"Disconnect": "Disconnect",
"EnableDisplayMirroring": "Enable display mirroring",
"HeaderSelectPlayer": "Select Player",
"Disconnect": "\u0410\u0436\u044b\u0440\u0430\u0442\u0443",
"EnableDisplayMirroring": "\u0411\u0435\u0439\u043d\u0435\u043b\u0435\u0443\u0434\u0456\u04a3 \u0442\u0435\u043b\u043d\u04b1\u0441\u049b\u0430\u0441\u044b\u043d \u049b\u043e\u0441\u0443",
"HeaderSelectPlayer": "\u041e\u0439\u043d\u0430\u0442\u049b\u044b\u0448\u0442\u044b \u0442\u0430\u04a3\u0434\u0430\u0443",
"MessageUnlockAppWithPurchaseOrSupporter": "\u041e\u0441\u044b \u049b\u04b1\u0440\u0430\u043c\u0434\u0430\u0441\u0442\u044b \u0431\u0456\u0440 \u0436\u043e\u043b\u0493\u044b \u0441\u0430\u0442\u044b\u043f \u0430\u043b\u0443, \u043d\u0435\u043c\u0435\u0441\u0435 \u0431\u0435\u043b\u0441\u0435\u043d\u0434\u0456 Emby Premiere \u0436\u0430\u0437\u044b\u043b\u044b\u043c\u044b \u0430\u0440\u049b\u044b\u043b\u044b \u049b\u04b1\u0440\u0441\u0430\u0443\u0434\u0430\u043d \u0431\u043e\u0441\u0430\u0442\u0443.",
"MessageUnlockAppWithSupporter": "\u041e\u0441\u044b \u049b\u04b1\u0440\u0430\u043c\u0434\u0430\u0441\u0442\u044b \u0431\u0435\u043b\u0441\u0435\u043d\u0434\u0456 Emby Premiere \u0436\u0430\u0437\u044b\u043b\u044b\u043c\u044b \u0430\u0440\u049b\u044b\u043b\u044b \u049b\u04b1\u0440\u0441\u0430\u0443\u0434\u0430\u043d \u0431\u043e\u0441\u0430\u0442\u0443.",
"MessageToValidateSupporter": "\u0415\u0433\u0435\u0440 \u0441\u0456\u0437\u0434\u0435 \u0431\u0435\u043b\u0441\u0435\u043d\u0434\u0456 Emby Premiere \u0436\u0430\u0437\u044b\u043b\u044b\u043c\u044b \u0431\u043e\u043b\u0441\u0430, Emby Server \u0442\u0430\u049b\u0442\u0430\u0441\u044b\u043d\u0434\u0430\u0493\u044b Emby Premiere \u043e\u0440\u043d\u0430\u0442\u044b\u043b\u044b\u043f \u0442\u0435\u04a3\u0448\u0435\u043b\u0433\u0435\u043d\u0456\u043d\u0435 \u043a\u04e9\u0437 \u0436\u0435\u0442\u043a\u0456\u0437\u0456\u04a3\u0456\u0437. \u0411\u04b1\u043b \u0431\u0430\u0441\u0442\u044b \u043c\u04d9\u0437\u0456\u0440\u0434\u0435 Emby Premiere \u0434\u0435\u0433\u0435\u043d\u0434\u0456 \u043d\u04b1\u049b\u044b\u043f \u049b\u0430\u0442\u044b\u043d\u0430\u0443\u043b\u044b.",

View File

@ -1,8 +1,8 @@
{
"HeaderRemoteControl": "Remote Control",
"Disconnect": "Disconnect",
"EnableDisplayMirroring": "Enable display mirroring",
"HeaderSelectPlayer": "Select Player",
"Disconnect": "\u0420\u0430\u0437\u044a\u0435\u0434\u0438\u043d\u0438\u0442\u044c\u0441\u044f",
"EnableDisplayMirroring": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0434\u0443\u0431\u043b\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u0435 \u043e\u0442\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u0438\u044f",
"HeaderSelectPlayer": "\u0412\u044b\u0431\u043e\u0440 \u043f\u0440\u043e\u0438\u0433\u0440\u044b\u0432\u0430\u0442\u0435\u043b\u044f",
"MessageUnlockAppWithPurchaseOrSupporter": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043f\u043e\u0441\u0440\u0435\u0434\u0441\u0442\u0432\u043e\u043c \u043d\u0435\u0431\u043e\u043b\u044c\u0448\u043e\u0439 \u043e\u0434\u043d\u043e\u043a\u0440\u0430\u0442\u043d\u043e\u0439 \u043e\u043f\u043b\u0430\u0442\u044b, \u0438\u043b\u0438 \u0441 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0439 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u043e\u0439 Emby Premiere .",
"MessageUnlockAppWithSupporter": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0434\u0430\u043d\u043d\u044b\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0441 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0439 \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u043e\u0439 Emby Premiere.",
"MessageToValidateSupporter": "\u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0438\u043c\u0435\u0435\u0442\u0441\u044f \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044e\u0449\u0430\u044f \u043f\u043e\u0434\u043f\u0438\u0441\u043a\u0430 Emby Premiere, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e Emby Premiere \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0432 \u0432\u0430\u0448\u0435\u0439 \u041f\u0430\u043d\u0435\u043b\u0438 Emby Server, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043f\u043e \u0449\u0435\u043b\u0447\u043a\u0443 \u043f\u043e Emby Premiere \u0432 \u0433\u043b\u0430\u0432\u043d\u043e\u043c \u043c\u0435\u043d\u044e.",
@ -361,7 +361,7 @@
"MessageDidYouKnowCinemaMode": "\u0417\u043d\u0430\u0435\u0442\u0435 \u043b\u0438 \u0432\u044b, \u0447\u0442\u043e \u0441 Emby Premiere \u0432\u044b \u0441\u043c\u043e\u0436\u0435\u0442\u0435 \u0440\u0430\u0441\u0448\u0438\u0440\u0438\u0442\u044c \u044d\u0444\u0444\u0435\u043a\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430\u043c\u0438 \u043f\u043e\u0434\u043e\u0431\u043d\u044b\u043c\u0438 \u0420\u0435\u0436\u0438\u043c\u0443 \u043a\u0438\u043d\u043e\u0437\u0430\u043b\u0430?",
"MessageDidYouKnowCinemaMode2": "\u0420\u0435\u0436\u0438\u043c \u043a\u0438\u043d\u043e\u0437\u0430\u043b\u0430 \u0434\u0430\u0441\u0442 \u0432\u0430\u043c \u044d\u0444\u0444\u0435\u043a\u0442 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0433\u043e \u0437\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u0437\u0430\u043b\u0430 \u0441 \u0442\u0440\u0435\u0439\u043b\u0435\u0440\u0430\u043c\u0438 \u0438 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u043b\u044c\u043d\u044b\u043c\u0438 \u0437\u0430\u0441\u0442\u0430\u0432\u043a\u0430\u043c\u0438 \u043f\u0435\u0440\u0435\u0434 \u0444\u0438\u043b\u044c\u043c\u043e\u043c.",
"HeaderPlayMyMedia": "\u0412\u043e\u0441\u043f\u0440\u043e\u0438\u0437\u0432\u0435\u0441\u0442\u0438 \u043c\u043e\u0438 \u043c\u0435\u0434\u0438\u0430\u0434\u0430\u043d\u043d\u044b\u0435",
"HeaderDiscoverEmbyPremiere": "\u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 Emby Premiere",
"HeaderDiscoverEmbyPremiere": "\u041e\u0442\u043a\u0440\u043e\u0439\u0442\u0435 \u0434\u043b\u044f \u0441\u0435\u0431\u044f Emby Premiere",
"Items": "\u042d\u043b\u0435\u043c\u0435\u043d\u0442\u044b",
"OneChannel": "\u041e\u0434\u0438\u043d \u043a\u0430\u043d\u0430\u043b",
"ConfirmRemoveDownload": "\u0418\u0437\u044a\u044f\u0442\u044c \u0437\u0430\u0433\u0440\u0443\u0437\u043a\u0443?",

View File

@ -229,7 +229,7 @@
html += '</div>';
html += '<div class="fldQuality selectContainer hide">';
html += '<select is="emby-select" id="selectQuality" data-mini="true" required="required" label="' + globalize.translate('sharedcomponents#LabelQuality') + '">';
html += '<select is="emby-select" id="selectQuality" required="required" label="' + globalize.translate('sharedcomponents#LabelQuality') + '">';
html += '</select>';
html += '<div class="fieldDescription qualityDescription"></div>';
html += '</div>';
@ -479,7 +479,10 @@
fldQuality.classList.remove('hide');
}
if (selectQuality) {
selectQuality.setAttribute('required', 'required');
//selectQuality.setAttribute('required', 'required');
// This is a hack due to what appears to be a edge bug but it shoudln't matter as the list always has selectable items
selectQuality.removeAttribute('required');
}
} else {
if (fldQuality) {

View File

@ -1,6 +1,6 @@
{
"name": "hls.js",
"version": "0.6.17",
"version": "0.6.18",
"license": "Apache-2.0",
"description": "Media Source Extension - HLS library, by/for Dailymotion",
"homepage": "https://github.com/dailymotion/hls.js",
@ -16,11 +16,11 @@
"test",
"tests"
],
"_release": "0.6.17",
"_release": "0.6.18",
"_resolution": {
"type": "version",
"tag": "v0.6.17",
"commit": "51624f2b19db3d1e205188d541d0e379275eef2a"
"tag": "v0.6.18",
"commit": "69a7a73cbc5fa2a9022c0eef8ebb603de699c4a8"
},
"_source": "https://github.com/dailymotion/hls.js.git",
"_target": "^0.6.11",

View File

@ -185,7 +185,10 @@ Configuration parameters could be provided to hls.js upon instantiation of `Hls`
maxBufferSize: 60*1000*1000,
maxBufferHole: 0.5,
maxSeekHole: 2,
seekHoleNudgeDuration: 0.01,
lowBufferWatchdogPeriod: 0.5,
highBufferWatchdogPeriod: 3,
nudgeOffset: 0.1,
nudgeMaxRetry : 3,
maxFragLookUpTolerance: 0.2,
liveSyncDurationCount: 3,
liveMaxLatencyDurationCount: 10,
@ -317,10 +320,27 @@ In case no quality level with this criteria can be found (lets say for example t
max video loading delay used in automatic start level selection : in that mode ABR controller will ensure that video loading time (ie the time to fetch the first fragment at lowest quality level + the time to fetch the fragment at the appropriate quality level is less than ```maxLoadingDelay``` )
#### ```seekHoleNudgeDuration```
(default 0.01s)
#### ```lowBufferWatchdogPeriod```
(default 0.5s)
In case playback is still stalling although a seek over buffer hole just occured, hls.js will seek to next buffer start + (number of consecutive stalls * `seekHoleNudgeDuration`) to try to restore playback.
if media element is expected to play and if currentTime has not moved for more than ```lowBufferWatchdogPeriod``` and if there are less than `maxBufferHole` seconds buffered upfront, hls.js will try to nudge playhead to recover playback
#### ```highBufferWatchdogPeriod```
(default 3s)
if media element is expected to play and if currentTime has not moved for more than ```highBufferWatchdogPeriod``` and if there are more than `maxBufferHole` seconds buffered upfront, hls.js will try to nudge playhead to recover playback
#### ```nudgeOffset```
(default 0.1s)
In case playback continues to stall after first playhead nudging, currentTime will be nudged evenmore following nudgeOffset to try to restore playback.
media.currentTime += (nb nudge retry -1)*nudgeOffset
#### ```nudgeMaxRetry```
(default 3)
Max nb of nudge retries before hls.js raise a fatal BUFFER_STALLED_ERROR
#### `maxFragLookUpTolerance`

View File

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

View File

@ -78,9 +78,10 @@ design idea is pretty simple :
- trigger BUFFER_APPENDING on FRAG_PARSING_DATA
- once FRAG_PARSED is received an all segments have been appended (BUFFER_APPENDED) then buffer controller will recheck whether it needs to buffer more data.
- **monitor current playback quality level** (buffer controller maintains a map between media position and quality level)
- **monitor playback progress** : if playhead is not moving anymore although it should (video metadata is known and video is not ended, nor paused, nor in seeking state) and if we have less than 400ms buffered upfront, and if there is a new buffer range available upfront, less than config.maxSeekHole from currentTime, then hls.js will **jump over the buffer hole** and seek to the beginning of this new buffered range, to "unstuck" the playback.
400 ms is a "magic number" that has been set to overcome browsers not always stopping playback at the exact end of a buffered range.
- **monitor playback progress** : if playhead is not moving for more than `config.lowBufferWatchdogPeriod` although it should (video metadata is known and video is not ended, nor paused, nor in seeking state) and if we have less than 500ms buffered upfront, and if there is a new buffer range available upfront, less than `config.maxSeekHole` from currentTime, then hls.js will **jump over the buffer hole** and seek to the beginning of this new buffered range, to "unstuck" the playback.
500 ms is a "magic number" that has been set to overcome browsers not always stopping playback at the exact end of a buffered range.
these holes in media buffered are often encountered on stream discontinuity or on quality level switch. holes could be "large" especially if fragments are not starting with a keyframe.
if playhead is stuck for more than `config.highBufferWatchdogPeriod` second in a buffered area, hls.js will nudge currentTime until playback recovers (it will retry every seconds, and report a fatal error after config.maxNudgeRetry retries)
- convert non-fatal `FRAG_LOAD_ERROR`/`FRAG_LOAD_TIMEOUT`/`KEY_LOAD_ERROR`/`KEY_LOAD_TIMEOUT` error into fatal error when media position is not buffered and max load retry has been reached
- stream controller actions are scheduled by a tick timer (invoked every 100ms) and actions are controlled by a state machine.
- [src/controller/timeline-controller.js][]
@ -253,4 +254,5 @@ design idea is pretty simple :
- ```BUFFER_STALLED_ERROR``` is raised by [src/controller/stream-controller.js][] if playback is stalling because of buffer underrun
- ```BUFFER_FULL_ERROR``` is raised by [src/controller/buffer-controller.js][] if sourcebuffer is full
- ```BUFFER_SEEK_OVER_HOLE``` is raised by [src/controller/stream-controller.js][] when hls.js seeks over a buffer hole after playback stalls
- ```BUFFER_NUDGE_ON_STALL``` is raised by [src/controller/stream-controller.js][] when hls.js nudge currentTime (when playback is stuck for more than 1s in a buffered area)
- ```INTERNAL_EXCEPTION``` is raised by [src/event-handler.js][] when a runtime exception is triggered by an internal Hls event handler. this error is non-fatal.

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
{
"name": "hls.js",
"version": "0.6.17",
"version": "0.6.18",
"license": "Apache-2.0",
"description": "Media Source Extension - HLS library, by/for Dailymotion",
"homepage": "https://github.com/dailymotion/hls.js",
@ -27,7 +27,7 @@
"watch": "watchify --debug -s Hls src/index.js -t [babelify] -o dist/hls.js",
"pretest": "npm run lint",
"test": "mocha --compilers js:babel-register --recursive tests/unit",
"testfunc": "mocha --compilers js:babel-register --recursive tests/functional/auto --timeout 40000",
"testfunc": "mocha --compilers js:babel-register tests/functional/auto/hlsjs.js --timeout 40000",
"lint": "jshint src/",
"serve": "http-server -p 8000 .",
"open": "opener http://localhost:8000/demo/",
@ -36,15 +36,15 @@
},
"devDependencies": {
"arraybuffer-equal": "^1.0.4",
"babel-cli": "^6.18.0",
"babel-cli": "^6.22.1",
"babel-preset-es2015": "^6.18.0",
"babel-register": "^6.18.0",
"babelify": "^7.2.0",
"browserify": "^13.1.1",
"browserify": "^13.2.0",
"browserify-derequire": "^0.9.4",
"browserify-versionify": "^1.0.6",
"bundle-collapser": "^1.2.1",
"chromedriver": "^2.26.1",
"chromedriver": "^2.27.1",
"deep-strict-equal": "^0.2.0",
"exorcist": "^0.4.0",
"http-server": "^0.9.0",

View File

@ -164,7 +164,7 @@
@media all and (max-width: 440px) {
@media all and (max-width: 400px) {
.playlist .listItemMediaInfo {
display: none !important;

View File

@ -37,7 +37,7 @@
var html = '';
var isConnectMode = AppInfo.isNativeApp ? true : false;
var isConnectMode = Dashboard.isConnectMode();
var configPageUrl = configPage ? Dashboard.getConfigurationPageUrl(configPage.Name) : null;
var href = configPage && !isConnectMode ?

View File

@ -1011,7 +1011,7 @@
ticks *= value;
var item = currentItem;
if (item && item.Chapters && item.Chapters[0].ImageTag) {
if (item && item.Chapters && item.Chapters.length && item.Chapters[0].ImageTag) {
var html = getChapterBubbleHtml(connectionManager.getApiClient(item.ServerId), item, item.Chapters, ticks);
if (html) {