1
0
Fork 0
mirror of https://github.com/jellyfin/jellyfin-web synced 2025-03-30 19:56:21 +00:00

update hls playback

This commit is contained in:
Luke Pulverenti 2016-04-20 14:51:47 -04:00
parent 7919706532
commit 396b125d66
27 changed files with 438 additions and 728 deletions

View file

@ -16,12 +16,12 @@
}, },
"devDependencies": {}, "devDependencies": {},
"ignore": [], "ignore": [],
"version": "1.2.33", "version": "1.2.34",
"_release": "1.2.33", "_release": "1.2.34",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "1.2.33", "tag": "1.2.34",
"commit": "ec985dcf4603026e64521c54971693e8522194d4" "commit": "f8d7590edf6f17060cfa04a3f6359ca36aa9789e"
}, },
"_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

@ -318,9 +318,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
}); });
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "hls.js", "name": "hls.js",
"version": "0.5.22", "version": "0.5.23",
"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.23",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "v0.5.22", "tag": "v0.5.23",
"commit": "ed41f6bffac7c6e35963c8aa741e00be8edea2c8" "commit": "485756a33406adc4874027d9f20283935c61471c"
}, },
"_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

@ -97,7 +97,6 @@ each error is categorized by :
- ```Hls.ErrorDetails.FRAG_LOAD_TIMEOUT```raised when fragment loading fails because of a timeout - ```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_DECRYPT_ERROR```raised when fragment decryption fails
- ```Hls.ErrorDetails.FRAG_PARSING_ERROR```raised when fragment parsing 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_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_APPENDING_ERROR```raised when exception is raised during buffer appending
- ```Hls.ErrorDetails.BUFFER_STALLED_ERROR```raised when playback stalls because the buffer runs out - ```Hls.ErrorDetails.BUFFER_STALLED_ERROR```raised when playback stalls because the buffer runs out
@ -560,10 +559,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 +615,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 +632,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 +669,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

View file

@ -1,6 +1,6 @@
{ {
"name": "hls.js", "name": "hls.js",
"version": "0.6.1", "version": "0.5.23",
"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.23",
"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

@ -234,15 +234,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});
} }
} }
} }
@ -1047,8 +1045,9 @@ _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});
} }
} }
} else { } else {

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

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,7 +42,6 @@ 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,
@ -73,8 +67,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 +76,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 +129,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 +145,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

@ -32,14 +32,14 @@
"web-component-tester": "^4.0.0", "web-component-tester": "^4.0.0",
"webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0" "webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0"
}, },
"homepage": "https://github.com/polymerelements/iron-icon", "homepage": "https://github.com/PolymerElements/iron-icon",
"_release": "1.0.8", "_release": "1.0.8",
"_resolution": { "_resolution": {
"type": "version", "type": "version",
"tag": "v1.0.8", "tag": "v1.0.8",
"commit": "f36b38928849ef3853db727faa8c9ef104d611eb" "commit": "f36b38928849ef3853db727faa8c9ef104d611eb"
}, },
"_source": "git://github.com/polymerelements/iron-icon.git", "_source": "git://github.com/PolymerElements/iron-icon.git",
"_target": "^1.0.0", "_target": "^1.0.0",
"_originalSource": "polymerelements/iron-icon" "_originalSource": "PolymerElements/iron-icon"
} }

View file

@ -36,7 +36,7 @@
"tag": "v1.4.0", "tag": "v1.4.0",
"commit": "554f7418fdbd97688eb21518b5f8172167d53a95" "commit": "554f7418fdbd97688eb21518b5f8172167d53a95"
}, },
"_source": "git://github.com/polymerelements/iron-selector.git", "_source": "git://github.com/PolymerElements/iron-selector.git",
"_target": "^1.0.0", "_target": "^1.0.0",
"_originalSource": "polymerelements/iron-selector" "_originalSource": "PolymerElements/iron-selector"
} }

View file

@ -975,12 +975,13 @@
self.createStreamInfo('Video', item, mediaSource, startPosition).then(function (streamInfo) { self.createStreamInfo('Video', item, mediaSource, startPosition).then(function (streamInfo) {
var isHls = streamInfo.url.toLowerCase().indexOf('.m3u8') != -1; var urlLower = streamInfo.url.toLowerCase();
var isHls = urlLower.indexOf('.m3u8') != -1;
// Huge hack alert. Safari doesn't seem to like if the segments aren't available right away when playback starts // Huge hack alert. Safari doesn't seem to like if the segments aren't available right away when playback starts
// This will start the transcoding process before actually feeding the video url into the player // This will start the transcoding process before actually feeding the video url into the player
// Edit: Also seeing stalls from hls.js // Edit: Also seeing stalls from hls.js
if (!mediaSource.RunTimeTicks && isHls) { if ((!mediaSource.RunTimeTicks) && isHls) {
Dashboard.showLoadingMsg(); Dashboard.showLoadingMsg();
var hlsPlaylistUrl = streamInfo.url.replace('master.m3u8', 'live.m3u8'); var hlsPlaylistUrl = streamInfo.url.replace('master.m3u8', 'live.m3u8');
@ -990,11 +991,11 @@
url: hlsPlaylistUrl url: hlsPlaylistUrl
}).then(function () { }).then(function () {
Dashboard.hideLoadingMsg();
streamInfo.url = hlsPlaylistUrl; streamInfo.url = hlsPlaylistUrl;
// add a delay to continue building up the buffer. without this we see failures in safari mobile // add a delay to continue building up the buffer. without this we see failures in safari mobile
setTimeout(function () { setTimeout(function () {
Dashboard.hideLoadingMsg();
self.playVideoInternal(item, mediaSource, startPosition, streamInfo, callback); self.playVideoInternal(item, mediaSource, startPosition, streamInfo, callback);
}, 2000); }, 2000);

View file

@ -275,9 +275,7 @@ define(['appSettings', 'userSettings', 'appStorage'], function (appSettings, use
var currentSrc = (self.getCurrentSrc(mediaRenderer) || '').toLowerCase(); var currentSrc = (self.getCurrentSrc(mediaRenderer) || '').toLowerCase();
if (currentSrc.indexOf('.m3u8') != -1) { if (currentSrc.indexOf('.m3u8') != -1) {
if (currentSrc.indexOf('forcelivestream=true') == -1) { return true;
return true;
}
} else { } else {
var duration = mediaRenderer.duration(); var duration = mediaRenderer.duration();
return duration && !isNaN(duration) && duration != Number.POSITIVE_INFINITY && duration != Number.NEGATIVE_INFINITY; return duration && !isNaN(duration) && duration != Number.POSITIVE_INFINITY && duration != Number.NEGATIVE_INFINITY;
@ -747,11 +745,6 @@ define(['appSettings', 'userSettings', 'appStorage'], function (appSettings, use
if (mediaSource.TranscodingSubProtocol == 'hls') { if (mediaSource.TranscodingSubProtocol == 'hls') {
if (mediaUrl.toLowerCase().indexOf('forcelivestream=true') != -1) {
startPositionInSeekParam = 0;
startTimeTicksOffset = startPosition || 0;
}
contentType = 'application/x-mpegURL'; contentType = 'application/x-mpegURL';
} else { } else {