From 43e745ac2ec7cc6b6d7a714819f60ce7493b081d Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Sun, 5 May 2024 16:52:32 -0700 Subject: [PATCH 01/14] feat: streaming events and errors --- src/content-steering-controller.js | 17 +++++ src/dash-playlist-loader.js | 17 +++++ src/media-segment-request.js | 106 +++++++++++++++++++++++------ src/playback-watcher.js | 13 ++++ src/playlist-controller.js | 29 ++++++-- src/playlist-loader.js | 39 +++++++++-- src/rendition-mixin.js | 16 ++++- src/segment-loader.js | 50 +++++++++++++- src/segment-transmuxer.js | 4 +- src/source-updater.js | 9 ++- src/timeline-change-controller.js | 9 ++- 11 files changed, 268 insertions(+), 41 deletions(-) diff --git a/src/content-steering-controller.js b/src/content-steering-controller.js index fab61160b..61cac2134 100644 --- a/src/content-steering-controller.js +++ b/src/content-steering-controller.js @@ -169,7 +169,13 @@ export default class ContentSteeringController extends videojs.EventTarget { this.dispose(); return; } + const metadata = { + contentSteeringInfo: { + uri + } + }; + this.trigger({ type: 'contentsteeringloadstart', metadata }); this.request_ = this.xhr_({ uri, requestType: 'content-steering-manifest' @@ -205,9 +211,20 @@ export default class ContentSteeringController extends videojs.EventTarget { this.startTTLTimeout_(); return; } + this.trigger({ type: 'contentsteeringloadcomplete', metadata }); const steeringManifestJson = JSON.parse(this.request_.responseText); this.assignSteeringProperties_(steeringManifestJson); + const parsedMetadata = { + contentSteeringInfo: metadata.contentSteeringInfo, + contentSteeringManifest: { + version: this.steeringManifest.version, + reloadUri: this.steeringManifest.reloadUri, + priority: this.steeringManifest.priority + } + }; + + this.trigger({ type: 'contentsteeringparsed', metadata: parsedMetadata }); this.startTTLTimeout_(); }); } diff --git a/src/dash-playlist-loader.js b/src/dash-playlist-loader.js index 398230c2c..ce6141ffc 100644 --- a/src/dash-playlist-loader.js +++ b/src/dash-playlist-loader.js @@ -646,6 +646,13 @@ export default class DashPlaylistLoader extends EventTarget { } requestMain_(cb) { + const metadata = { + manifestInfo: { + uri: this.mainPlaylistLoader_.srcUrl + } + }; + + this.trigger({type: 'manifestrequeststart', metadata}); this.request = this.vhs_.xhr({ uri: this.mainPlaylistLoader_.srcUrl, withCredentials: this.withCredentials, @@ -657,6 +664,7 @@ export default class DashPlaylistLoader extends EventTarget { } return; } + this.trigger({type: 'manifestrequestcomplete', metadata}); const mainChanged = req.responseText !== this.mainPlaylistLoader_.mainXml_; @@ -762,7 +770,13 @@ export default class DashPlaylistLoader extends EventTarget { this.mediaRequest_ = null; const oldMain = this.mainPlaylistLoader_.main; + const metadata = { + manifestInfo: { + uri: this.mainPlaylistLoader_.srcUrl + } + }; + this.trigger({type: 'manifestparsestart', metadata}); let newMain = parseMainXml({ mainXml: this.mainPlaylistLoader_.mainXml_, srcUrl: this.mainPlaylistLoader_.srcUrl, @@ -789,6 +803,9 @@ export default class DashPlaylistLoader extends EventTarget { } this.addEventStreamToMetadataTrack_(newMain); + metadata.parsedManifest = newMain; + // TODO: Do we want to pass the entire parsed manifest here or just select parts? + this.trigger({type: 'manifestparsecomplete', metadata}); return Boolean(newMain); } diff --git a/src/media-segment-request.js b/src/media-segment-request.js index 5ec7131e9..caa55069b 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -121,7 +121,7 @@ const handleErrors = (error, request) => { * @param {Function} finishProcessingFn - a callback to execute to continue processing * this request */ -const handleKeyResponse = (segment, objects, finishProcessingFn) => (error, request) => { +const handleKeyResponse = (segment, objects, finishProcessingFn, triggerSegmentEventFn) => (error, request) => { const response = request.response; const errorObj = handleErrors(error, request); @@ -149,7 +149,9 @@ const handleKeyResponse = (segment, objects, finishProcessingFn) => (error, requ for (let i = 0; i < objects.length; i++) { objects[i].bytes = bytes; } + const keyInfo = { uri: request.uri }; + triggerSegmentEventFn({ type: 'segmentkeyloadcomplete', keyInfo }); return finishProcessingFn(null, segment); }; @@ -212,7 +214,7 @@ const parseInitSegment = (segment, callback) => { * this request */ const handleInitSegmentResponse = -({segment, finishProcessingFn}) => (error, request) => { +({segment, finishProcessingFn, triggerSegmentEventFn}) => (error, request) => { const errorObj = handleErrors(error, request); if (errorObj) { @@ -220,6 +222,7 @@ const handleInitSegmentResponse = } const bytes = new Uint8Array(request.response); + triggerSegmentEventFn({ type: 'segmentloaded', isInit: true }); // init segment is encypted, we will have to wait // until the key request is done to decrypt. if (segment.map.key) { @@ -254,14 +257,15 @@ const handleInitSegmentResponse = const handleSegmentResponse = ({ segment, finishProcessingFn, - responseType + responseType, + triggerSegmentEventFn }) => (error, request) => { const errorObj = handleErrors(error, request); if (errorObj) { return finishProcessingFn(errorObj, segment); } - + triggerSegmentEventFn({ type: 'segmentloaded' }); const newBytes = // although responseText "should" exist, this guard serves to prevent an error being // thrown for two primary cases: @@ -296,7 +300,8 @@ const transmuxAndNotify = ({ endedTimelineFn, dataFn, doneFn, - onTransmuxerLog + onTransmuxerLog, + triggerSegmentEventFn }) => { const fmp4Tracks = segment.map && segment.map.tracks || {}; const isMuxed = Boolean(fmp4Tracks.audio && fmp4Tracks.video); @@ -325,6 +330,12 @@ const transmuxAndNotify = ({ trackInfo.isMuxed = true; } trackInfoFn(segment, trackInfo); + const info = { + hasAudio: trackInfo.hasAudio, + hasVideo: trackInfo.hasVideo + }; + + triggerSegmentEventFn({ type: 'segmenttransmuxingtrackinfoavailable', trackInfo: info }); } }, onAudioTimingInfo: (audioTimingInfo) => { @@ -350,9 +361,33 @@ const transmuxAndNotify = ({ } }, onVideoSegmentTimingInfo: (videoSegmentTimingInfo) => { + const timingInfo = { + pts: { + start: videoSegmentTimingInfo.start.pts, + end: videoSegmentTimingInfo.end.pts + }, + dts: { + start: videoSegmentTimingInfo.start.dts, + end: videoSegmentTimingInfo.end.dts + } + }; + + triggerSegmentEventFn({ type: 'segmenttransmuxingtiminginfoavailable', timingInfo }); videoSegmentTimingInfoFn(videoSegmentTimingInfo); }, onAudioSegmentTimingInfo: (audioSegmentTimingInfo) => { + const timingInfo = { + pts: { + start: audioSegmentTimingInfo.start.pts, + end: audioSegmentTimingInfo.end.pts + }, + dts: { + start: audioSegmentTimingInfo.start.dts, + end: audioSegmentTimingInfo.end.dts + } + }; + + triggerSegmentEventFn({ type: 'segmenttransmuxingtiminginfoavailable', timingInfo }); audioSegmentTimingInfoFn(audioSegmentTimingInfo); }, onId3: (id3Frames, dispatchType) => { @@ -371,8 +406,10 @@ const transmuxAndNotify = ({ return; } result.type = result.type === 'combined' ? 'video' : result.type; + triggerSegmentEventFn({ type: 'segmenttransmuxingcomplete' }); doneFn(null, segment, result); - } + }, + triggerSegmentEventFn }); // In the transmuxer, we don't yet have the ability to extract a "proper" start time. @@ -415,7 +452,8 @@ const handleSegmentBytes = ({ endedTimelineFn, dataFn, doneFn, - onTransmuxerLog + onTransmuxerLog, + triggerSegmentEventFn }) => { let bytesAsUint8Array = new Uint8Array(bytes); @@ -565,7 +603,8 @@ const handleSegmentBytes = ({ endedTimelineFn, dataFn, doneFn, - onTransmuxerLog + onTransmuxerLog, + triggerSegmentEventFn }); }; @@ -642,15 +681,19 @@ const decryptSegment = ({ endedTimelineFn, dataFn, doneFn, - onTransmuxerLog + onTransmuxerLog, + triggerSegmentEventFn }) => { + triggerSegmentEventFn({ type: 'segmentdecryptionstart' }); decrypt({ id: segment.requestId, key: segment.key, encryptedBytes: segment.encryptedBytes, - decryptionWorker + decryptionWorker, + triggerSegmentEventFn }, (decryptedBytes) => { segment.bytes = decryptedBytes; + triggerSegmentEventFn({ type: 'segmentdecryptioncomplete' }); handleSegmentBytes({ segment, @@ -665,7 +708,8 @@ const decryptSegment = ({ endedTimelineFn, dataFn, doneFn, - onTransmuxerLog + onTransmuxerLog, + triggerSegmentEventFn }); }); }; @@ -712,7 +756,8 @@ const waitForCompletion = ({ endedTimelineFn, dataFn, doneFn, - onTransmuxerLog + onTransmuxerLog, + triggerSegmentEventFn }) => { let count = 0; let didError = false; @@ -759,7 +804,8 @@ const waitForCompletion = ({ endedTimelineFn, dataFn, doneFn, - onTransmuxerLog + onTransmuxerLog, + triggerSegmentEventFn }); } // Otherwise, everything is ready just continue @@ -776,13 +822,15 @@ const waitForCompletion = ({ endedTimelineFn, dataFn, doneFn, - onTransmuxerLog + onTransmuxerLog, + triggerSegmentEventFn }); }; // Keep track of when *all* of the requests have completed segment.endOfAllRequests = Date.now(); if (segment.map && segment.map.encryptedBytes && !segment.map.bytes) { + triggerSegmentEventFn({ type: 'segmentdecryptionstart', isInit: true }); return decrypt({ decryptionWorker, // add -init to the "id" to differentiate between segment @@ -790,10 +838,11 @@ const waitForCompletion = ({ // at the same time at some point in the future. id: segment.requestId + '-init', encryptedBytes: segment.map.encryptedBytes, - key: segment.map.key + key: segment.map.key, + triggerSegmentEventFn }, (decryptedBytes) => { segment.map.bytes = decryptedBytes; - + triggerSegmentEventFn({ type: 'segmentdecryptioncomplete', isInit: true }); parseInitSegment(segment, (parseError) => { if (parseError) { abortAll(activeXhrs); @@ -966,7 +1015,8 @@ export const mediaSegmentRequest = ({ endedTimelineFn, dataFn, doneFn, - onTransmuxerLog + onTransmuxerLog, + triggerSegmentEventFn }) => { const activeXhrs = []; const finishProcessingFn = waitForCompletion({ @@ -982,7 +1032,8 @@ export const mediaSegmentRequest = ({ endedTimelineFn, dataFn, doneFn, - onTransmuxerLog + onTransmuxerLog, + triggerSegmentEventFn }); // optionally, request the decryption key @@ -997,7 +1048,10 @@ export const mediaSegmentRequest = ({ responseType: 'arraybuffer', requestType: 'segment-key' }); - const keyRequestCallback = handleKeyResponse(segment, objects, finishProcessingFn); + const keyRequestCallback = handleKeyResponse(segment, objects, finishProcessingFn, triggerSegmentEventFn); + const keyInfo = { uri: segment.key.resolvedUri }; + + triggerSegmentEventFn({ type: 'segmentkeyloadstart', keyInfo }); const keyXhr = xhr(keyRequestOptions, keyRequestCallback); activeXhrs.push(keyXhr); @@ -1013,7 +1067,10 @@ export const mediaSegmentRequest = ({ responseType: 'arraybuffer', requestType: 'segment-key' }); - const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn); + const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn, triggerSegmentEventFn); + const keyInfo = { uri: segment.key.resolvedUri }; + + triggerSegmentEventFn({ type: 'segmentkeyloadstart', keyInfo }); const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback); activeXhrs.push(mapKeyXhr); @@ -1024,7 +1081,9 @@ export const mediaSegmentRequest = ({ headers: segmentXhrHeaders(segment.map), requestType: 'segment-media-initialization' }); - const initSegmentRequestCallback = handleInitSegmentResponse({segment, finishProcessingFn}); + const initSegmentRequestCallback = handleInitSegmentResponse({segment, finishProcessingFn, triggerSegmentEventFn}); + + triggerSegmentEventFn({ type: 'segmentloadstart', isInit: true }); const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); activeXhrs.push(initSegmentXhr); @@ -1040,8 +1099,11 @@ export const mediaSegmentRequest = ({ const segmentRequestCallback = handleSegmentResponse({ segment, finishProcessingFn, - responseType: segmentRequestOptions.responseType + responseType: segmentRequestOptions.responseType, + triggerSegmentEventFn }); + + triggerSegmentEventFn({ type: 'segmentloadstart' }); const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); segmentXhr.addEventListener( diff --git a/src/playback-watcher.js b/src/playback-watcher.js index 509959efe..e8772990d 100644 --- a/src/playback-watcher.js +++ b/src/playback-watcher.js @@ -11,6 +11,7 @@ import window from 'global/window'; import * as Ranges from './ranges'; import logger from './util/logger'; +import { createTimeRange } from 'video.js/dist/types/utils/time'; // Set of events that reset the playback-watcher time check logic and clear the timeout const timerCancelEvents = [ @@ -38,6 +39,7 @@ export default class PlaybackWatcher { this.allowSeeksWithinUnsafeLiveWindow = options.allowSeeksWithinUnsafeLiveWindow; this.liveRangeSafeTimeDelta = options.liveRangeSafeTimeDelta; this.media = options.media; + this.playedRanges_ = []; this.consecutiveUpdates = 0; this.lastRecordedTime = null; @@ -205,6 +207,11 @@ export default class PlaybackWatcher { // the buffered value for this loader changed // appends are working if (isBufferedDifferent) { + const metadata = { + bufferedRanges: buffered + }; + + pc.trigger({ type: 'bufferedrangeschanged', metadata }); this.resetSegmentDownloads_(type); return; } @@ -271,6 +278,12 @@ export default class PlaybackWatcher { } else if (currentTime === this.lastRecordedTime) { this.consecutiveUpdates++; } else { + this.playedRanges_.push(createTimeRange(this.lastRecordedTime, currentTime)); + const metadata = { + playedRanges: this.playedRanges_ + }; + + this.playlistController_.trigger({ type: 'playedrangeschanged', metadata }); this.consecutiveUpdates = 0; this.lastRecordedTime = currentTime; } diff --git a/src/playlist-controller.js b/src/playlist-controller.js index 4af9996e0..fa683d61c 100644 --- a/src/playlist-controller.js +++ b/src/playlist-controller.js @@ -409,7 +409,20 @@ export class PlaylistController extends videojs.EventTarget { if (oldId && oldId !== newId) { this.logger_(`switch media ${oldId} -> ${newId} from ${cause}`); - this.tech_.trigger({type: 'usage', name: `vhs-rendition-change-${cause}`}); + const metadata = { + renditionInfo: { + id: newId, + bandwidth: playlist.attributes.BANDWIDTH, + resolution: { + width: playlist.attributes.RESOLUTION.width, + height: playlist.attributes.RESOLUTION.height + }, + codecs: playlist.attributes.CODECS + }, + cause + }; + + this.tech_.trigger({type: 'usage', name: `vhs-rendition-change-${cause}`, metadata}); } this.mainPlaylistLoader_.media(playlist, delay); } @@ -726,11 +739,11 @@ export class PlaylistController extends videojs.EventTarget { } }); - this.mainPlaylistLoader_.on('renditiondisabled', () => { - this.tech_.trigger({type: 'usage', name: 'vhs-rendition-disabled'}); + this.mainPlaylistLoader_.on('renditiondisabled', (metadata) => { + this.tech_.trigger({type: 'usage', name: 'vhs-rendition-disabled', metadata}); }); - this.mainPlaylistLoader_.on('renditionenabled', () => { - this.tech_.trigger({type: 'usage', name: 'vhs-rendition-enabled'}); + this.mainPlaylistLoader_.on('renditionenabled', (metadata) => { + this.tech_.trigger({type: 'usage', name: 'vhs-rendition-enabled', metadata}); }); } @@ -1633,8 +1646,11 @@ export class PlaylistController extends videojs.EventTarget { } this.logger_(`seekable updated [${Ranges.printableRange(this.seekable_)}]`); + const metadata = { + seekableRanges: this.seekable_ + }; - this.tech_.trigger('seekablechanged'); + this.tech_.trigger({ type: 'seekablechanged', metadata }); } /** @@ -1781,6 +1797,7 @@ export class PlaylistController extends videojs.EventTarget { return true; } + // find from and to for codec switch event getCodecsOrExclude_() { const media = { main: this.mainSegmentLoader_.getCurrentMediaInfo_() || {}, diff --git a/src/playlist-loader.js b/src/playlist-loader.js index 31094ae0c..36d8ea9e2 100644 --- a/src/playlist-loader.js +++ b/src/playlist-loader.js @@ -522,6 +522,14 @@ export default class PlaylistLoader extends EventTarget { this.request = null; this.state = 'HAVE_METADATA'; + const metadata = { + playlistInfo: { + type: 'media', + uri: url + } + }; + + this.trigger({type: 'playlistparsestart', metadata }); const playlist = playlistObject || this.parseManifest_({ url, manifestString: playlistString @@ -550,7 +558,8 @@ export default class PlaylistLoader extends EventTarget { } this.updateMediaUpdateTimeout_(refreshDelay(this.media(), !!update)); - + metadata.parsedPlaylist = playlist; + this.trigger({ type: 'playlistparsecomplete', metadata }); this.trigger('loadedplaylist'); } @@ -690,6 +699,14 @@ export default class PlaylistLoader extends EventTarget { } this.pendingMedia_ = playlist; + const metadata = { + playlistInfo: { + type: 'media', + uri: playlist.uri + } + }; + + this.trigger({ type: 'playlistrequeststart', metadata }); this.request = this.vhs_.xhr({ uri: playlist.resolvedUri, @@ -709,6 +726,8 @@ export default class PlaylistLoader extends EventTarget { return this.playlistRequestError(this.request, playlist, startingState); } + this.trigger({ type: 'playlistrequestcomplete', metadata }); + this.haveMetadata({ playlistString: req.responseText, url: playlist.uri, @@ -836,7 +855,14 @@ export default class PlaylistLoader extends EventTarget { }, 0); return; } + const metadata = { + playlistInfo: { + type: 'multivariant', + uri: this.src + } + }; + this.trigger({ type: 'playlistrequeststart', metadata }); // request the specified URL this.request = this.vhs_.xhr({ uri: this.src, @@ -857,24 +883,27 @@ export default class PlaylistLoader extends EventTarget { message: `HLS playlist request error at URL: ${this.src}.`, responseText: req.responseText, // MEDIA_ERR_NETWORK - code: 2, - metadata: { - errorType: videojs.Error.HlsPlaylistRequestError - } + code: 2 }; if (this.state === 'HAVE_NOTHING') { this.started = false; } return this.trigger('error'); } + this.trigger({ type: 'playlistrequestcomplete', metadata }); this.src = resolveManifestRedirect(this.src, req); + this.trigger({ type: 'playlistparsestart', metadata }); const manifest = this.parseManifest_({ manifestString: req.responseText, url: this.src }); + // TODO: Do we want to pass the entire parsed manifest here or just select fields? + metadata.parsedPlaylist = manifest; + this.trigger({ type: 'playlistparsecomplete', metadata }); + this.setupInitialPlaylist(manifest); }); } diff --git a/src/rendition-mixin.js b/src/rendition-mixin.js index a889c9c65..e250264e6 100644 --- a/src/rendition-mixin.js +++ b/src/rendition-mixin.js @@ -27,14 +27,26 @@ const enableFunction = (loader, playlistID, changePlaylistFn) => (enable) => { } else { playlist.disabled = true; } + const metadata = { + renditionInfo: { + id: playlistID, + bandwidth: playlist.attributes.BANDWIDTH, + resolution: { + width: playlist.attributes.RESOLUTION.width, + height: playlist.attributes.RESOLUTION.height + }, + codecs: playlist.attributes.CODECS + }, + cause: 'fast-quality' + }; if (enable !== currentlyEnabled && !incompatible) { // Ensure the outside world knows about our changes changePlaylistFn(playlist); if (enable) { - loader.trigger('renditionenabled'); + loader.trigger({ type: 'renditionenabled', metadata }); } else { - loader.trigger('renditiondisabled'); + loader.trigger({ type: 'renditiondisabled', metadata }); } } return enable; diff --git a/src/segment-loader.js b/src/segment-loader.js index c761d157e..8eee6b902 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -510,6 +510,23 @@ export const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => return null; }; +/** + * Utility function to reduce a segmentInfo object to a simple event payload. + * + * @param {SegmentInfo} segmentInfo the full SegmentInfo object to be reduced. + * @return the reduced payload. + */ +const segmentInfoPayload = (segmentInfo, isInit = false) => { + return { + type: this.loaderType_, + uri: segmentInfo.uri, + start: segmentInfo.startOfSegment, + duration: segmentInfo.duration, + isEncrypted: Boolean(segmentInfo.segment.key), + isMediaInitialization: isInit + }; +}; + /** * An object that manages segment loading and appending. * @@ -667,7 +684,8 @@ export default class SegmentLoader extends videojs.EventTarget { // since its loads follow main, needs to listen on timeline changes. For more details, // see the shouldWaitForTimelineChange function. if (this.loaderType_ === 'audio') { - this.timelineChangeController_.on('timelinechange', () => { + this.timelineChangeController_.on('timelinechange', (metadata) => { + this.trigger({type: 'timelinechange', metadata }); if (this.hasEnoughInfoToLoad_()) { this.processLoadQueue_(); } @@ -1384,6 +1402,11 @@ bufferedEnd: ${lastBufferedEnd(this.buffered_())} if (!segmentInfo) { return; } + const metadata = { + segmentInfo: segmentInfoPayload(segmentInfo) + }; + + this.trigger({ type: 'segmentselected', metadata }); if (typeof segmentInfo.timestampOffset === 'number') { this.isPendingTimestampOffset_ = false; @@ -2464,7 +2487,11 @@ Fetch At Buffer: ${this.fetchAtBuffer_} segments }); } + const metadata = { + segmentInfo: segmentInfoPayload(segmentInfo, Boolean(initSegment)) + }; + this.trigger({ type: 'segmentappendstart', metadata }); this.sourceUpdater_.appendBuffer( {segmentInfo, type, bytes}, this.handleAppendError_.bind(this, {segmentInfo, type, bytes}) @@ -2628,11 +2655,16 @@ ${segmentInfoString(segmentInfo)}`); this.logger_('received endedtimeline callback'); }, id3Fn: this.handleId3_.bind(this), - dataFn: this.handleData_.bind(this), doneFn: this.segmentRequestFinished_.bind(this), onTransmuxerLog: ({message, level, stream}) => { this.logger_(`${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}`); + }, + triggerSegmentEventFn: ({ type, isInit, keyInfo, trackInfo, timingInfo }) => { + const segInfo = segmentInfoPayload(segmentInfo, isInit); + const metadata = { segmentInfo: segInfo, keyInfo, trackInfo, timingInfo }; + + this.trigger({ type, metadata }); } }); } @@ -2746,6 +2778,14 @@ ${segmentInfoString(segmentInfo)}`); ` is less than the min to record ${MIN_SEGMENT_DURATION_TO_SAVE_STATS}`); return; } + const metadata = { + bandwidthInfo: { + from: this.bandwidth, + to: stats.bandwidth + } + }; + + this.trigger({type: 'usage', name: 'vhs-bandwidth-update', metadata }); this.bandwidth = stats.bandwidth; this.roundTrip = stats.roundTripTime; @@ -3108,7 +3148,11 @@ ${segmentInfoString(segmentInfo)}`); handleAppendsDone_() { // appendsdone can cause an abort if (this.pendingSegment_) { - this.trigger('appendsdone'); + const metadata = { + segmentInfo: segmentInfoPayload(this.pendingSegment_) + }; + + this.trigger({ type: 'appendsdone', metadata}); } if (!this.pendingSegment_) { diff --git a/src/segment-transmuxer.js b/src/segment-transmuxer.js index 6d1bb789e..c57398da5 100644 --- a/src/segment-transmuxer.js +++ b/src/segment-transmuxer.js @@ -82,7 +82,8 @@ export const processTransmux = (options) => { onDone, onEndedTimeline, onTransmuxerLog, - isEndOfTimeline + isEndOfTimeline, + triggerSegmentEventFn } = options; const transmuxedData = { buffer: [] @@ -182,6 +183,7 @@ export const processTransmux = (options) => { const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer; const byteOffset = bytes instanceof ArrayBuffer ? 0 : bytes.byteOffset; + triggerSegmentEventFn('segmenttransmuxingstart'); transmuxer.postMessage( { action: 'push', diff --git a/src/source-updater.js b/src/source-updater.js index 48c1d889c..28bd7862e 100644 --- a/src/source-updater.js +++ b/src/source-updater.js @@ -284,8 +284,15 @@ const actions = { if (oldCodecBase === newCodecBase) { return; } + const metadata = { + codecsChangeInfo: { + from: oldCodec, + to: codec + } + }; - sourceUpdater.logger_(`changing ${type}Buffer codec from ${sourceUpdater.codecs[type]} to ${codec}`); + sourceUpdater.trigger({ type: 'codecschange', metadata }); + sourceUpdater.logger_(`changing ${type}Buffer codec from ${oldCodec} to ${codec}`); // check if change to the provided type is supported try { diff --git a/src/timeline-change-controller.js b/src/timeline-change-controller.js index 90cc794cd..109c88b19 100644 --- a/src/timeline-change-controller.js +++ b/src/timeline-change-controller.js @@ -34,7 +34,14 @@ export default class TimelineChangeController extends videojs.EventTarget { if (typeof from === 'number' && typeof to === 'number') { this.lastTimelineChanges_[type] = { type, from, to }; delete this.pendingTimelineChanges_[type]; - this.trigger('timelinechange'); + const metadata = { + timelineChangeInfo: { + from, + to + } + }; + + this.trigger({ type: 'timelinechange', metadata }); } return this.lastTimelineChanges_[type]; } From 981f5ad71e94b9896f10708c59a61bce19141f4c Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Mon, 6 May 2024 01:00:58 -0700 Subject: [PATCH 02/14] chore: add more errors --- src/content-steering-controller.js | 13 +++++++- src/dash-playlist-loader.js | 53 +++++++++++++++++++++--------- src/error-codes.js | 27 +++++++++++++++ src/media-segment-request.js | 20 ++++++++--- src/playlist-loader.js | 32 +++++++++++------- src/segment-loader.js | 21 ++++++++---- src/vtt-segment-loader.js | 3 +- src/xhr.js | 1 + 8 files changed, 129 insertions(+), 41 deletions(-) diff --git a/src/content-steering-controller.js b/src/content-steering-controller.js index 61cac2134..d3ee8e75c 100644 --- a/src/content-steering-controller.js +++ b/src/content-steering-controller.js @@ -212,7 +212,18 @@ export default class ContentSteeringController extends videojs.EventTarget { return; } this.trigger({ type: 'contentsteeringloadcomplete', metadata }); - const steeringManifestJson = JSON.parse(this.request_.responseText); + let steeringManifestJson; + + try { + steeringManifestJson = JSON.parse(this.request_.responseText); + } catch (parseError) { + const errorMetadata = { + errorType: videojs.Error.StreamingContentSteeringParserError, + error: parseError + }; + + this.trigger({ type: 'error', metadata: errorMetadata }); + } this.assignSteeringProperties_(steeringManifestJson); const parsedMetadata = { diff --git a/src/dash-playlist-loader.js b/src/dash-playlist-loader.js index ce6141ffc..24b445776 100644 --- a/src/dash-playlist-loader.js +++ b/src/dash-playlist-loader.js @@ -22,6 +22,7 @@ import containerRequest from './util/container-request.js'; import {toUint8} from '@videojs/vhs-utils/es/byte-helpers'; import logger from './util/logger'; import {merge} from './util/vjs-compat'; +import { getStreamingNetworkErrorMetadata } from './error-codes.js'; const { EventTarget } = videojs; @@ -391,7 +392,12 @@ export default class DashPlaylistLoader extends EventTarget { const uri = resolveManifestRedirect(playlist.sidx.resolvedUri); const fin = (err, request) => { - // TODO: add error metdata here once we create an error type in video.js + const { requestType } = request; + + if (err) { + err.metadata = getStreamingNetworkErrorMetadata({ requestType, request, error: err }); + } + if (this.requestErrored_(err, request, startingState)) { return; } @@ -402,9 +408,7 @@ export default class DashPlaylistLoader extends EventTarget { try { sidx = parseSidx(toUint8(request.response).subarray(8)); } catch (e) { - e.metadata = { - errorType: videojs.Error.DashManifestSidxParsingError - }; + e.metadata = getStreamingNetworkErrorMetadata({requestType, request, parseFailure: true }); // sidx parsing failed. this.requestErrored_(e, request, startingState); @@ -439,11 +443,7 @@ export default class DashPlaylistLoader extends EventTarget { internal: true, playlistExclusionDuration: Infinity, // MEDIA_ERR_NETWORK - code: 2, - metadata: { - errorType: videojs.Error.UnsupportedSidxContainer, - sidxContainer - } + code: 2 }, request); } @@ -462,6 +462,7 @@ export default class DashPlaylistLoader extends EventTarget { this.request = this.vhs_.xhr({ uri, responseType: 'arraybuffer', + requestType: 'dash-sidx', headers: segmentXhrHeaders({byterange: playlist.sidx.byterange}) }, fin); }); @@ -658,6 +659,12 @@ export default class DashPlaylistLoader extends EventTarget { withCredentials: this.withCredentials, requestType: 'dash-manifest' }, (error, req) => { + if (error) { + const { requestType } = req; + + error.metadata = getStreamingNetworkErrorMetadata({ requestType, request: req, error }); + } + if (this.requestErrored_(error, req)) { if (this.state === 'HAVE_NOTHING') { this.started = false; @@ -725,6 +732,9 @@ export default class DashPlaylistLoader extends EventTarget { } if (error) { + const { requestType } = req; + + this.error.metadata = getStreamingNetworkErrorMetadata({ requestType, request: req, error }); // sync request failed, fall back to using date header from mpd // TODO: log warning this.mainPlaylistLoader_.clientOffset_ = this.mainLoaded_ - Date.now(); @@ -777,13 +787,24 @@ export default class DashPlaylistLoader extends EventTarget { }; this.trigger({type: 'manifestparsestart', metadata}); - let newMain = parseMainXml({ - mainXml: this.mainPlaylistLoader_.mainXml_, - srcUrl: this.mainPlaylistLoader_.srcUrl, - clientOffset: this.mainPlaylistLoader_.clientOffset_, - sidxMapping: this.mainPlaylistLoader_.sidxMapping_, - previousManifest: oldMain - }); + let newMain; + + try { + newMain = parseMainXml({ + mainXml: this.mainPlaylistLoader_.mainXml_, + srcUrl: this.mainPlaylistLoader_.srcUrl, + clientOffset: this.mainPlaylistLoader_.clientOffset_, + sidxMapping: this.mainPlaylistLoader_.sidxMapping_, + previousManifest: oldMain + }); + } catch (error) { + this.error = error; + this.error.metadata = { + errorType: videojs.Error.StreamingDashManifestParserError, + error + }; + this.trigger('error'); + } // if we have an old main to compare the new main against if (oldMain) { diff --git a/src/error-codes.js b/src/error-codes.js index 241c0c3e7..dc2b41df5 100644 --- a/src/error-codes.js +++ b/src/error-codes.js @@ -1,2 +1,29 @@ +import videojs from 'video.js'; + // https://www.w3.org/TR/WebIDL-1/#quotaexceedederror export const QUOTA_EXCEEDED_ERR = 22; + +export const getStreamingNetworkErrorMetadata = ({ requestType, request, error, parseFailure }) => { + const isBadStatus = request.status < 200 || request.status > 299; + const errorMetadata = { + uri: request.uri, + requestType + }; + + if (error) { + errorMetadata.error = error; + errorMetadata.errorType = videojs.Error.NetworkRequestFailed; + } else if (request.timedout) { + errorMetadata.errorType = videojs.Error.NetworkRequestTimeout; + } else if (request.aborted) { + errorMetadata.erroType = videojs.Error.NetworkRequestAborted; + } else if (parseFailure || isBadStatus) { + const errorType = parseFailure ? videojs.Error.NetworkBodyParserFailed : videojs.Error.NetworkBadStatus; + + errorMetadata.errorType = errorType; + errorMetadata.status = request.status; + errorMetadata.headers = request.headers; + } + + return errorMetadata; +}; diff --git a/src/media-segment-request.js b/src/media-segment-request.js index caa55069b..6337f42c7 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -9,6 +9,7 @@ import { isLikelyFmp4MediaSegment } from '@videojs/vhs-utils/es/containers'; import {merge} from './util/vjs-compat'; +import { getStreamingNetworkErrorMetadata } from './error-codes.js'; export const REQUEST_ERRORS = { FAILURE: 2, @@ -72,12 +73,16 @@ const getProgressStats = (progressEvent) => { * @param {Object} request - the XHR request that possibly generated the error */ const handleErrors = (error, request) => { + const { requestType } = request; + const metadata = getStreamingNetworkErrorMetadata({ requestType, request, error }); + if (request.timedout) { return { status: request.status, message: 'HLS request timed-out at URL: ' + request.uri, code: REQUEST_ERRORS.TIMEOUT, - xhr: request + xhr: request, + metadata }; } @@ -86,7 +91,8 @@ const handleErrors = (error, request) => { status: request.status, message: 'HLS request aborted at URL: ' + request.uri, code: REQUEST_ERRORS.ABORTED, - xhr: request + xhr: request, + metadata }; } @@ -95,7 +101,8 @@ const handleErrors = (error, request) => { status: request.status, message: 'HLS request errored at URL: ' + request.uri, code: REQUEST_ERRORS.FAILURE, - xhr: request + xhr: request, + metadata }; } @@ -104,7 +111,8 @@ const handleErrors = (error, request) => { status: request.status, message: 'Empty HLS response at URL: ' + request.uri, code: REQUEST_ERRORS.FAILURE, - xhr: request + xhr: request, + metadata }; } @@ -623,7 +631,9 @@ const decrypt = function({id, key, encryptedBytes, decryptionWorker}, callback) }; decryptionWorker.addEventListener('message', decryptionHandler); - + decryptionWorker.addEventListener('error', () => { + // call StreamingFailedToDecryptSegmentError here. + }); let keyBytes; if (key.bytes.slice) { diff --git a/src/playlist-loader.js b/src/playlist-loader.js index 36d8ea9e2..0de5b898f 100644 --- a/src/playlist-loader.js +++ b/src/playlist-loader.js @@ -21,6 +21,7 @@ import { import {getKnownPartCount} from './playlist.js'; import {merge} from './util/vjs-compat'; import DateRangesStorage from './util/date-ranges'; +import { getStreamingNetworkErrorMetadata } from './error-codes.js'; const { EventTarget } = videojs; @@ -486,23 +487,29 @@ export default class PlaylistLoader extends EventTarget { message: `HLS playlist request error at URL: ${uri}.`, responseText: xhr.responseText, code: (xhr.status >= 500) ? 4 : 2, - metadata: { - errorType: videojs.Error.HlsPlaylistRequestError - } + metadata: getStreamingNetworkErrorMetadata({ requestType: xhr.requestType, xhr, error: xhr.error }) }; this.trigger('error'); } parseManifest_({url, manifestString}) { - return parseManifest({ - onwarn: ({message}) => this.logger_(`m3u8-parser warn for ${url}: ${message}`), - oninfo: ({message}) => this.logger_(`m3u8-parser info for ${url}: ${message}`), - manifestString, - customTagParsers: this.customTagParsers, - customTagMappers: this.customTagMappers, - llhls: this.llhls - }); + try { + return parseManifest({ + onwarn: ({message}) => this.logger_(`m3u8-parser warn for ${url}: ${message}`), + oninfo: ({message}) => this.logger_(`m3u8-parser info for ${url}: ${message}`), + manifestString, + customTagParsers: this.customTagParsers, + customTagMappers: this.customTagMappers, + llhls: this.llhls + }); + } catch (error) { + this.error = error; + this.error.metadata = { + errorType: videojs.Error.StreamingHlsPlaylistParserError, + error + }; + } } /** @@ -883,7 +890,8 @@ export default class PlaylistLoader extends EventTarget { message: `HLS playlist request error at URL: ${this.src}.`, responseText: req.responseText, // MEDIA_ERR_NETWORK - code: 2 + code: 2, + metadata: getStreamingNetworkErrorMetadata({ requestType: req.requestType, request: req, error }) }; if (this.state === 'HAVE_NOTHING') { this.started = false; diff --git a/src/segment-loader.js b/src/segment-loader.js index 8eee6b902..a72a49671 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -516,9 +516,9 @@ export const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => * @param {SegmentInfo} segmentInfo the full SegmentInfo object to be reduced. * @return the reduced payload. */ -const segmentInfoPayload = (segmentInfo, isInit = false) => { +const segmentInfoPayload = (type, segmentInfo, isInit = false) => { return { - type: this.loaderType_, + type, uri: segmentInfo.uri, start: segmentInfo.startOfSegment, duration: segmentInfo.duration, @@ -1403,7 +1403,7 @@ bufferedEnd: ${lastBufferedEnd(this.buffered_())} return; } const metadata = { - segmentInfo: segmentInfoPayload(segmentInfo) + segmentInfo: segmentInfoPayload(this.loaderType_, segmentInfo) }; this.trigger({ type: 'segmentselected', metadata }); @@ -1519,6 +1519,15 @@ Fetch At Buffer: ${this.fetchAtBuffer_} const syncInfo = this.getSyncInfoFromMediaSequenceSync_(targetTime); if (!syncInfo) { + const message = 'No sync info found while using media sequence sync'; + + this.error({ + message, + metadata: { + errorType: videojs.Error.StreamingFailedToSelectNextSegment, + error: new Error(message) + } + }); this.logger_('chooseNextRequest_ - no sync info found using media sequence sync'); // no match return null; @@ -2488,7 +2497,7 @@ Fetch At Buffer: ${this.fetchAtBuffer_} }); } const metadata = { - segmentInfo: segmentInfoPayload(segmentInfo, Boolean(initSegment)) + segmentInfo: segmentInfoPayload(this.loaderType_, segmentInfo, Boolean(initSegment)) }; this.trigger({ type: 'segmentappendstart', metadata }); @@ -2661,7 +2670,7 @@ ${segmentInfoString(segmentInfo)}`); this.logger_(`${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}`); }, triggerSegmentEventFn: ({ type, isInit, keyInfo, trackInfo, timingInfo }) => { - const segInfo = segmentInfoPayload(segmentInfo, isInit); + const segInfo = segmentInfoPayload(this.loaderType_, segmentInfo, isInit); const metadata = { segmentInfo: segInfo, keyInfo, trackInfo, timingInfo }; this.trigger({ type, metadata }); @@ -3149,7 +3158,7 @@ ${segmentInfoString(segmentInfo)}`); // appendsdone can cause an abort if (this.pendingSegment_) { const metadata = { - segmentInfo: segmentInfoPayload(this.pendingSegment_) + segmentInfo: segmentInfoPayload(this.loaderType_, this.pendingSegment_) }; this.trigger({ type: 'appendsdone', metadata}); diff --git a/src/vtt-segment-loader.js b/src/vtt-segment-loader.js index 2af32fe30..bdc0bed15 100644 --- a/src/vtt-segment-loader.js +++ b/src/vtt-segment-loader.js @@ -330,7 +330,8 @@ export default class VTTSegmentLoader extends SegmentLoader { this.stopForError({ message: e.message, metadata: { - errorType: videojs.Error.VttCueParsingError + errorType: videojs.Error.StreamingVttParserError, + error: e } }); return; diff --git a/src/xhr.js b/src/xhr.js index aebe6fdad..0b8d300e3 100644 --- a/src/xhr.js +++ b/src/xhr.js @@ -132,6 +132,7 @@ const xhrFactory = function() { return originalAbort.apply(request, arguments); }; request.uri = options.uri; + request.requestType = options.requestType; request.requestTime = Date.now(); return request; }; From 22db463e4ec9affe482182ff839c588f72b0ad85 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Mon, 6 May 2024 19:58:22 -0700 Subject: [PATCH 03/14] chore: add remaining errors and refactor event payloads --- package-lock.json | 406 +++++++++++++++++------------------ package.json | 2 +- src/media-segment-request.js | 72 ++++--- src/playback-watcher.js | 4 +- src/segment-loader.js | 40 +++- src/segment-transmuxer.js | 17 +- src/source-updater.js | 5 + 7 files changed, 301 insertions(+), 245 deletions(-) diff --git a/package-lock.json b/package-lock.json index c201b689d..32679789e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1868,13 +1868,13 @@ "JSV": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz", - "integrity": "sha512-ZJ6wx9xaKJ3yFUhq5/sk82PJMuUyLk277I8mQeyDgCTjGdjWJIvPfaU5LIXaMuaN2UO1X3kZH4+lgphublZUHw==", + "integrity": "sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c=", "dev": true }, "abbrev": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha512-LEyx4aLEC3x6T0UguF6YILf+ntvmOaWsVfENmIW0E9H09vKlLDGelMjjSm0jkDHALj8A8quZ/HapKNigzwge+Q==", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", "dev": true }, "accepts": { @@ -1902,7 +1902,7 @@ "add-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz", - "integrity": "sha512-qQLMr+8o0WC4FZGQTcJiKBVC59JylcPSrTtk6usvmIDFUOCKegapy1VHQwRbFMOFyb/inzUVqHs+eMYKDM1YeQ==", + "integrity": "sha1-anmQQ3ynNtXhKI25K9MmbV9csqo=", "dev": true }, "aes-decrypter": { @@ -1962,7 +1962,7 @@ "amdefine": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true, "optional": true }, @@ -1986,7 +1986,7 @@ "ansi": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz", - "integrity": "sha512-iFY7JCgHbepc0b82yLaw4IMortylNb6wG4kL+4R0C3iv6i+RHGHux/yUX5BTiRvSX/shMnngjR1YyNMnXEFh5A==", + "integrity": "sha1-DELU+xcWDVqa8eSEus4cZpIsGyE=", "dev": true }, "ansi-colors": { @@ -2091,19 +2091,19 @@ "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", "dev": true }, "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", - "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=", "dev": true }, "arrify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true }, "astral-regex": { @@ -2115,7 +2115,7 @@ "async": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=", "dev": true }, "axios": { @@ -2216,7 +2216,7 @@ "binary": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", - "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=", "dev": true, "requires": { "buffers": "~0.1.1", @@ -2395,7 +2395,7 @@ "buffers": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", - "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=", "dev": true }, "builtin-modules": { @@ -2484,7 +2484,7 @@ "chainsaw": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", - "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=", "dev": true, "requires": { "traverse": ">=0.3.0 <0.4" @@ -2493,7 +2493,7 @@ "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", - "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=", "dev": true } } @@ -2584,7 +2584,7 @@ "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, "string-width": { @@ -2600,7 +2600,7 @@ "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "dev": true, "requires": { "ansi-regex": "^3.0.0" @@ -2638,7 +2638,7 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, "color-convert": { @@ -2653,7 +2653,7 @@ "color-name": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, "colorette": { @@ -2683,7 +2683,7 @@ "commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, "compare-func": { @@ -2782,7 +2782,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true } } @@ -3099,7 +3099,7 @@ "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", "dev": true, "requires": { "array-find-index": "^1.0.1" @@ -3108,19 +3108,19 @@ "custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=", "dev": true }, "cycle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/cycle/-/cycle-1.0.3.tgz", - "integrity": "sha512-TVF6svNzeQCOpjCqsy0/CSy8VgObG3wXusJ73xW2GbG5rGx7lC8zxDSURicsXI2UsGdi2L0QNRCi745/wUDvsA==", + "integrity": "sha1-IegLK+hYD5i0aPN5QwZisEbDStI=", "dev": true }, "d3": { "version": "3.5.17", "resolved": "https://registry.npmjs.org/d3/-/d3-3.5.17.tgz", - "integrity": "sha512-yFk/2idb8OHPKkbAL8QaOaqENNoMhIaSHZerk3oQsECwkObkCpJyjYwCe+OHiq6UEdhe1m8ZGARRRO3ljFjlKg==", + "integrity": "sha1-vEZ0gAQ3iyGjYMn8fPUjF5B2L7g=", "dev": true }, "dargs": { @@ -3153,7 +3153,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, "decamelize-keys": { @@ -3204,7 +3204,7 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", "dev": true }, "depd": { @@ -3222,7 +3222,7 @@ "di": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=", "dev": true }, "diff": { @@ -3399,7 +3399,7 @@ "dom-serialize": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", "dev": true, "requires": { "custom-event": "~1.0.0", @@ -3475,7 +3475,7 @@ "duplexer2": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", - "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", "dev": true, "requires": { "readable-stream": "^2.0.2" @@ -3516,7 +3516,7 @@ "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, "electron-to-chromium": { @@ -3534,7 +3534,7 @@ "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=", "dev": true }, "engine.io": { @@ -3581,7 +3581,7 @@ "ent": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=", "dev": true }, "entities": { @@ -3658,7 +3658,7 @@ "es6-promisify": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", - "integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", "dev": true, "requires": { "es6-promise": "^4.0.3" @@ -3673,19 +3673,19 @@ "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=", "dev": true }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, "escodegen": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha512-yhi5S+mNTOuRvyW4gWlg5W1byMaQGWWSYHXsuFZ7GBo7tpyOwi2EdzMP/QWxh9hwkD2m+wDVHJsxhRIj+v/b/A==", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", "dev": true, "requires": { "esprima": "^2.7.1", @@ -3698,7 +3698,7 @@ "source-map": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha512-CBdZ2oa/BHhS4xj5DlhjWNHcan57/5YuvfdLf17iVmIpd9KRm+DFLmC6nBNj+6Ua7Kt3TmOjDpQT1aTYOQtoUA==", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", "dev": true, "optional": true, "requires": { @@ -3985,7 +3985,7 @@ "esprima": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha512-OarPfz0lFCiW4/AV2Oy1Rp9qu0iusTKqykwTspGCZtPxmF81JR4MmIebvF1F9+UOKth2ZubLQ4XGGaU+hSn99A==", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", "dev": true }, "esquery": { @@ -4025,7 +4025,7 @@ "estraverse": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha512-25w1fMXQrGdoquWnScXZGckOv+Wes+JDnuN/+7ex3SauFRS72r2lFDec0EKPt2YD1wUJ/IrfEex+9yp4hfSOJA==", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", "dev": true }, "estree-walker": { @@ -4043,7 +4043,7 @@ "etag": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", "dev": true }, "eventemitter3": { @@ -4084,7 +4084,7 @@ "external-editor": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-1.1.1.tgz", - "integrity": "sha512-0XYlP43jzxMgJjugDJ85Z0UDPnowkUbfFztNvsSGC9sJVIk97MZbGEb9WAhIVH0UgNxoLj/9ZQgB4CHJyz2GGQ==", + "integrity": "sha1-Etew24UPf/fnCBuvQAVwAGDEYAs=", "dev": true, "requires": { "extend": "^3.0.0", @@ -4095,7 +4095,7 @@ "tmp": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.29.tgz", - "integrity": "sha512-89PTqMWGDva+GqClOqBV9s3SMh7MA3Mq0pJUdAoHuF65YoE7O0LermaZkVfT5/Ngfo18H4eYiyG7zKOtnEbxsw==", + "integrity": "sha1-8lEl/w3Z2jzLDC3Tce4SiLuRKMA=", "dev": true, "requires": { "os-tmpdir": "~1.0.1" @@ -4106,7 +4106,7 @@ "eyes": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/eyes/-/eyes-0.1.8.tgz", - "integrity": "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ==", + "integrity": "sha1-Ys8SAjTGg3hdkCNIqADvPgzCC8A=", "dev": true }, "fast-deep-equal": { @@ -4124,7 +4124,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, "fault": { @@ -4139,7 +4139,7 @@ "figures": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==", + "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", "dev": true, "requires": { "escape-string-regexp": "^1.0.5", @@ -4191,7 +4191,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true } } @@ -4254,13 +4254,13 @@ "format": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "integrity": "sha1-1hcBB+nv3E7TDJ3DkBbflCtctYs=", "dev": true }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", "dev": true }, "fs-extra": { @@ -4283,7 +4283,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", "dev": true }, "fsevents": { @@ -4334,13 +4334,13 @@ "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", "dev": true }, "gauge": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/gauge/-/gauge-1.2.7.tgz", - "integrity": "sha512-fVbU2wRE91yDvKUnrIaQlHKAWKY5e08PmztCrwuH5YVQ+Z/p3d0ny2T48o6uvAAXHIUnfaQdHkmxYbQft1eHVA==", + "integrity": "sha1-6c7FSD09TuDvRLYKfZnkk14TbZM=", "dev": true, "requires": { "ansi": "^0.3.0", @@ -4436,7 +4436,7 @@ "get-stdin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", "dev": true }, "get-stream": { @@ -4471,7 +4471,7 @@ "git-remote-origin-url": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", - "integrity": "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", "dev": true, "requires": { "gitconfiglocal": "^1.0.0", @@ -4499,7 +4499,7 @@ "gitconfiglocal": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", - "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", "dev": true, "requires": { "ini": "^1.3.2" @@ -4508,7 +4508,7 @@ "github-url-from-git": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/github-url-from-git/-/github-url-from-git-1.5.0.tgz", - "integrity": "sha512-WWOec4aRI7YAykQ9+BHmzjyNlkfJFG8QLXnDTsLz/kZefq7qkzdfo4p6fkYYMIq1aj+gZcQs/1HQhQh3DPPxlQ==", + "integrity": "sha1-+YX+3MCpqledyI16/waNVcxiUaA=", "dev": true }, "glob": { @@ -4604,7 +4604,7 @@ "has-ansi": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -4613,7 +4613,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true } } @@ -4627,13 +4627,13 @@ "has-color": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", - "integrity": "sha512-kaNz5OTAYYmt646Hkqw50/qyxP2vFnTVu5AQ1Zmk22Kk5+4Qx6BpO8+u7IKsML5fOsFk0ZT0AcCJNYwcvaLBvw==", + "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", "dev": true }, "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", "dev": true }, "has-symbols": { @@ -4654,7 +4654,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", "dev": true }, "hosted-git-info": { @@ -4822,7 +4822,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", "dev": true }, "indent-string": { @@ -4839,7 +4839,7 @@ "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { "once": "^1.3.0", @@ -4861,7 +4861,7 @@ "inquirer": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-1.2.3.tgz", - "integrity": "sha512-diSnpgfv/Ozq6QKuV2mUcwZ+D24b03J3W6EVxzvtkCWJTPrH2gKLsqgSW0vzRMZZFhFdhnvzka0RUJxIm7AOxQ==", + "integrity": "sha1-TexvMvN+97sLLtPx0aXD9UUHSRg=", "dev": true, "requires": { "ansi-escapes": "^1.1.0", @@ -4883,25 +4883,25 @@ "ansi-escapes": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", - "integrity": "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw==", + "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", "dev": true }, "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", "dev": true }, "ansi-styles": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, "chalk": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { "ansi-styles": "^2.2.1", @@ -4914,7 +4914,7 @@ "cli-cursor": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", - "integrity": "sha512-25tABq090YNKkF6JH7lcwO0zFJTRke4Jcq9iX2nr/Sz0Cjjv4gckmwlW6Ty/aoyFd6z3ysR2hMGC2GFugmBo6A==", + "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", "dev": true, "requires": { "restore-cursor": "^1.0.1" @@ -4923,13 +4923,13 @@ "exit-hook": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", - "integrity": "sha512-MsG3prOVw1WtLXAZbM3KiYtooKR1LvxHh3VHsVtIy0uiUu8usxgB/94DP2HxtD/661lLdB6yzQ09lGJSQr6nkg==", + "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", "dev": true }, "is-fullwidth-code-point": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, "requires": { "number-is-nan": "^1.0.0" @@ -4938,13 +4938,13 @@ "onetime": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", - "integrity": "sha512-GZ+g4jayMqzCRMgB2sol7GiCLjKfS1PINkjmx8spcKce1LiVqcbQreXwqs2YAFXC6R03VIG28ZS31t8M866v6A==", + "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "dev": true }, "restore-cursor": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", - "integrity": "sha512-reSjH4HuiFlxlaBaFCiS6O76ZGG2ygKoSlCsipKdaZuKSPx/+bt9mULkn4l0asVzbEfQQmXRg6Wp6gv6m0wElw==", + "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", "dev": true, "requires": { "exit-hook": "^1.0.0", @@ -4954,7 +4954,7 @@ "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, "requires": { "code-point-at": "^1.0.0", @@ -4965,7 +4965,7 @@ "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, "requires": { "ansi-regex": "^2.0.0" @@ -4974,7 +4974,7 @@ "supports-color": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", "dev": true } } @@ -5015,7 +5015,7 @@ "is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", "dev": true }, "is-bigint": { @@ -5103,7 +5103,7 @@ "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", "dev": true }, "is-finite": { @@ -5141,7 +5141,7 @@ "is-module": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", - "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", "dev": true }, "is-negative-zero": { @@ -5174,7 +5174,7 @@ "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", "dev": true }, "is-reference": { @@ -5199,7 +5199,7 @@ "is-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", + "integrity": "sha1-/S2INUXEa6xaYz57mgnof6LLUGk=", "dev": true }, "is-shared-array-buffer": { @@ -5235,7 +5235,7 @@ "is-text-path": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", - "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", "dev": true, "requires": { "text-extensions": "^1.0.0" @@ -5244,7 +5244,7 @@ "is-utf8": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", "dev": true }, "is-weakref": { @@ -5268,7 +5268,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", "dev": true }, "isbinaryfile": { @@ -5280,19 +5280,19 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", "dev": true }, "isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", "dev": true }, "istanbul": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha512-nMtdn4hvK0HjUlzr1DrKSUY8ychprt8dzHOgY2KXsIhHu5PuQQEOTM27gV9Xblyon7aUH/TSFIjRHEODF/FRPg==", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", "dev": true, "requires": { "abbrev": "1.0.x", @@ -5314,7 +5314,7 @@ "glob": { "version": "5.0.15", "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha512-c9IPMazfRITpmAAKi22dK1VKxGDX9ehhqfABDriL/lzO92xcUKEJPQHrVA/2YHSNFB4iFlykVmWvwo48nr3OxA==", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", "dev": true, "requires": { "inflight": "^1.0.4", @@ -5327,7 +5327,7 @@ "has-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha512-DyYHfIYwAJmjAjSSPKANxI8bFY9YtFrgkAfinBojQ8YJTOuOuav64tMUJv584SES4xl74PmuaevIyaLESHdTAA==", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", "dev": true }, "mkdirp": { @@ -5342,13 +5342,13 @@ "resolve": { "version": "1.1.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha512-9znBF0vBcaSN3W2j7wKvdERPwqTxSpCq+if5C0WoTCyV9n24rua28jeuQ2pL/HOf+yUe/Mef+H/5p60K0Id3bg==", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", "dev": true }, "supports-color": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha512-Jds2VIYDrlp5ui7t8abHN2bjAu4LV/q4N2KivFPpGH0lrka0BMq/33AmECUXlKPcHigkNaqfXRENFju+rlcy+A==", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", "dev": true, "requires": { "has-flag": "^1.0.0" @@ -5532,13 +5532,13 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", "dev": true }, "json5": { @@ -5550,7 +5550,7 @@ "jsonfile": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", "dev": true, "requires": { "graceful-fs": "^4.1.6" @@ -5569,7 +5569,7 @@ "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", - "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=", "dev": true }, "just-extend": { @@ -5668,13 +5668,13 @@ "camelcase": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", "dev": true }, "camelcase-keys": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { "camelcase": "^2.0.0", @@ -5684,7 +5684,7 @@ "dateformat": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", - "integrity": "sha512-5sFRfAAmbHdIts+eKjR9kYJoF0ViCMVX9yqLu5A7S/v+nd077KgCITOMiirmyCBiZpKLDXbBOkYm6tu7rX/TKg==", + "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=", "dev": true, "requires": { "get-stdin": "^4.0.1", @@ -5694,7 +5694,7 @@ "find-up": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", "dev": true, "requires": { "path-exists": "^2.0.0", @@ -5710,7 +5710,7 @@ "indent-string": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, "requires": { "repeating": "^2.0.0" @@ -5719,7 +5719,7 @@ "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -5732,13 +5732,13 @@ "map-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", "dev": true }, "meow": { "version": "3.7.0", "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { "camelcase-keys": "^2.0.0", @@ -5768,7 +5768,7 @@ "parse-json": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", "dev": true, "requires": { "error-ex": "^1.2.0" @@ -5777,7 +5777,7 @@ "path-exists": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", "dev": true, "requires": { "pinkie-promise": "^2.0.0" @@ -5786,7 +5786,7 @@ "path-type": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -5797,7 +5797,7 @@ "read-pkg": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", "dev": true, "requires": { "load-json-file": "^1.0.0", @@ -5808,7 +5808,7 @@ "read-pkg-up": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", "dev": true, "requires": { "find-up": "^1.0.0", @@ -5818,7 +5818,7 @@ "redent": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", "dev": true, "requires": { "indent-string": "^2.1.0", @@ -5834,13 +5834,13 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", "dev": true }, "strip-bom": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", "dev": true, "requires": { "is-utf8": "^0.2.0" @@ -5849,7 +5849,7 @@ "strip-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", "dev": true, "requires": { "get-stdin": "^4.0.1" @@ -5858,7 +5858,7 @@ "trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", "dev": true } } @@ -5896,7 +5896,7 @@ "karma-ie-launcher": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/karma-ie-launcher/-/karma-ie-launcher-1.0.0.tgz", - "integrity": "sha512-ts71ke8pHvw6qdRtq0+7VY3ANLoZuUNNkA8abRaWV13QRPNm7TtSOqyszjHUtuwOWKcsSz4tbUtrNICrQC+SXQ==", + "integrity": "sha1-SXmGhCxJAZA0bNifVJTKmDDG1Zw=", "dev": true, "requires": { "lodash": "^4.6.1" @@ -5955,7 +5955,7 @@ "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", "dev": true, "requires": { "prelude-ls": "~1.1.2", @@ -6019,7 +6019,7 @@ "listenercount": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", - "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc=", "dev": true }, "listr2": { @@ -6058,7 +6058,7 @@ "load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -6070,7 +6070,7 @@ "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", "dev": true, "requires": { "error-ex": "^1.3.1", @@ -6080,7 +6080,7 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true } } @@ -6103,13 +6103,13 @@ "lodash-compat": { "version": "3.10.2", "resolved": "https://registry.npmjs.org/lodash-compat/-/lodash-compat-3.10.2.tgz", - "integrity": "sha512-k8SE/OwvWfYZqx3MA/Ry1SHBDWre8Z8tCs0Ba0bF5OqVNvymxgFZ/4VDtbTxzTvcoG11JpTMFsaeZp/yGYvFnA==", + "integrity": "sha1-xpQBKKnTD46QLNLPmf0Muk7PwYM=", "dev": true }, "lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, "lodash.clonedeep": { @@ -6121,25 +6121,25 @@ "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", "dev": true }, "lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", + "integrity": "sha1-nMtOUF1Ia5FlE0V3KIWi3yf9AXw=", "dev": true }, "lodash.get": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", "dev": true }, "lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", - "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==", + "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=", "dev": true }, "lodash.merge": { @@ -6151,31 +6151,31 @@ "lodash.pad": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/lodash.pad/-/lodash.pad-4.5.1.tgz", - "integrity": "sha512-mvUHifnLqM+03YNzeTBS1/Gr6JRFjd3rRx88FHWUvamVaT9k2O/kXha3yBSOwB9/DTQrSTLJNHvLBBt2FdX7Mg==", + "integrity": "sha1-QzCUmoM6fI2iLMIPaibE1Z3runA=", "dev": true }, "lodash.padend": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", - "integrity": "sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==", + "integrity": "sha1-U8y6BH0G4VjTEfRdpiX05J5vFm4=", "dev": true }, "lodash.padstart": { "version": "4.6.1", "resolved": "https://registry.npmjs.org/lodash.padstart/-/lodash.padstart-4.6.1.tgz", - "integrity": "sha512-sW73O6S8+Tg66eY56DBk85aQzzUJDtpoXFBgELMd5P/SotAguo+1kYO6RuYgXxA4HJH3LFTFPASX6ET6bjfriw==", + "integrity": "sha1-0uPuv/DZ05rVD1y9G1KnvOa7YRs=", "dev": true }, "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", "dev": true }, "lodash.uniq": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", "dev": true }, "log-update": { @@ -6280,7 +6280,7 @@ "loud-rejection": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha512-RPNliZOFkqFumDhvYqOaNY4Uz9oJM2K9tC6JWsJJsNdhuONW4LQHRBpb0qf4pJApVffI5N39SwzWZJuEhfd7eQ==", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", "dev": true, "requires": { "currently-unhandled": "^0.4.1", @@ -6598,19 +6598,19 @@ "mdurl": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", "dev": true }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "dev": true }, "memorystream": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/memorystream/-/memorystream-0.3.1.tgz", - "integrity": "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==", + "integrity": "sha1-htcJCzDORV1j+64S3aUaR93K+bI=", "dev": true }, "meow": { @@ -6852,7 +6852,7 @@ "min-document": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "integrity": "sha1-e9KC4/WELtKVu3SM3Z8f+iyCRoU=", "requires": { "dom-walk": "^0.1.0" } @@ -6921,7 +6921,7 @@ "mute-stream": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.6.tgz", - "integrity": "sha512-m0kBTDLF/0lgzCsPVmJSKM5xkLNX7ZAB0Q+n2DP37JMIRPVC2R4c3BdO6x++bXFKftbhvSfKgwxAexME+BRDRw==", + "integrity": "sha1-SJYrGeFp/R38JAs/HnMXYnu8R9s=", "dev": true }, "mux.js": { @@ -6936,7 +6936,7 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, "negotiator": { @@ -6986,7 +6986,7 @@ "nomnom": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", - "integrity": "sha512-5s0JxqhDx9/rksG2BTMVN1enjWSvPidpoSgViZU4ZXULyTe+7jxcCRLB6f42Z0l1xYJpleCBtSyY6Lwg3uu5CQ==", + "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", "dev": true, "requires": { "chalk": "~0.4.0", @@ -6996,13 +6996,13 @@ "ansi-styles": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", - "integrity": "sha512-3iF4FIKdxaVYT3JqQuY3Wat/T2t7TRbbQ94Fu50ZUCbLy4TFbTzr90NOHQodQkNqmeEGCw8WbeP78WNi6SKYUA==", + "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", "dev": true }, "chalk": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", - "integrity": "sha512-sQfYDlfv2DGVtjdoQqxS0cEZDroyG8h6TamA6rvxwlrU5BaSLDx9xhatBYl2pxZ7gmpNaPFVwBtdGdu5rQ+tYQ==", + "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", "dev": true, "requires": { "ansi-styles": "~1.0.0", @@ -7013,13 +7013,13 @@ "strip-ansi": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", - "integrity": "sha512-behete+3uqxecWlDAm5lmskaSaISA+ThQ4oNNBDTBJt0x2ppR6IPqfZNuj6BLaLJ/Sji4TPZlcRyOis8wXQTLg==", + "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", "dev": true }, "underscore": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ==", + "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", "dev": true } } @@ -7036,7 +7036,7 @@ "nopt": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", "dev": true, "requires": { "abbrev": "1" @@ -7119,7 +7119,7 @@ "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, "semver": { @@ -7131,7 +7131,7 @@ "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, "requires": { "shebang-regex": "^1.0.0" @@ -7140,7 +7140,7 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, "which": { @@ -7166,7 +7166,7 @@ "npmlog": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-2.0.4.tgz", - "integrity": "sha512-DaL6RTb8Qh4tMe2ttPT1qWccETy2Vi5/8p+htMpLBeXJTr2CAqnF5WQtSP2eFpvaNbhLZ5uilDb98mRm4Q+lZQ==", + "integrity": "sha1-mLUlMPJRTKkNCexbIsiEZyI3VpI=", "dev": true, "requires": { "ansi": "~0.3.1", @@ -7177,13 +7177,13 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", "dev": true }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", "dev": true }, "object-inspect": { @@ -7222,7 +7222,7 @@ "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { "wrappy": "1" @@ -7277,19 +7277,19 @@ "os-shim": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/os-shim/-/os-shim-0.1.3.tgz", - "integrity": "sha512-jd0cvB8qQ5uVt0lvCIexBaROw1KyKm5sbulg2fWOHjETisuCzWyt+eTZKEMs8v6HwzoGs8xik26jg7eCM6pS+A==", + "integrity": "sha1-a2LDeRz3kJ6jXtRuF2WLtBfLORc=", "dev": true }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "dev": true }, "p-limit": { @@ -7361,7 +7361,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, "path-key": { @@ -7388,7 +7388,7 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", "dev": true } } @@ -7405,7 +7405,7 @@ "pify": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", "dev": true } } @@ -7437,13 +7437,13 @@ "pinkie": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", "dev": true }, "pinkie-promise": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { "pinkie": "^2.0.0" @@ -7517,7 +7517,7 @@ "prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, "prettyjson": { @@ -7533,7 +7533,7 @@ "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=" }, "process-nextick-args": { "version": "2.0.1", @@ -7556,7 +7556,7 @@ "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", "dev": true }, "qjobs": { @@ -7668,7 +7668,7 @@ "read-pkg": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha512-BLq/cCO9two+lBgiTYNqD6GdtK8s4NpaWrl6/rCO9w0TUS8oJl7cmToOZfRYllKTISY6nt1U7jQ53brmKqY6BA==", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", "dev": true, "requires": { "load-json-file": "^4.0.0", @@ -7705,7 +7705,7 @@ "read-pkg-up": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha512-YFzFrVvpC6frF1sz8psoHDBGF7fLPc+llq/8NB43oagqWkx8ar5zYtsTORtOjw9W2RHLpWP+zTWwBvf1bCmcSw==", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", "dev": true, "requires": { "find-up": "^2.0.0", @@ -7715,7 +7715,7 @@ "find-up": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", "dev": true, "requires": { "locate-path": "^2.0.0" @@ -7724,7 +7724,7 @@ "locate-path": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", "dev": true, "requires": { "p-locate": "^2.0.0", @@ -7743,7 +7743,7 @@ "p-locate": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", "dev": true, "requires": { "p-limit": "^1.1.0" @@ -7752,13 +7752,13 @@ "p-try": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", "dev": true }, "path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true } } @@ -7786,7 +7786,7 @@ "rechoir": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", "dev": true, "requires": { "resolve": "^1.1.6" @@ -7903,13 +7903,13 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true }, "repeating": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha512-ZqtSMuVybkISo2OWvqvm7iHSWngvdaW3IpsT9/uP8v4gMi591LY6h35wdOfvQdWCKFWZWm2Y1Opp4kV7vQKT6A==", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, "requires": { "is-finite": "^1.0.0" @@ -7918,7 +7918,7 @@ "require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, "require-from-string": { @@ -7930,7 +7930,7 @@ "requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=", "dev": true }, "requizzle": { @@ -8111,19 +8111,19 @@ "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", "dev": true }, "is-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, "requires": { "path-key": "^2.0.0" @@ -8132,7 +8132,7 @@ "path-key": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", "dev": true }, "semver": { @@ -8144,7 +8144,7 @@ "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, "requires": { "shebang-regex": "^1.0.0" @@ -8153,7 +8153,7 @@ "shebang-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, "which": { @@ -8184,7 +8184,7 @@ "rx": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", + "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=", "dev": true }, "rxjs": { @@ -8228,7 +8228,7 @@ "semver-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==", + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", "dev": true }, "semver-regex": { @@ -8313,7 +8313,7 @@ "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", "dev": true }, "setprototypeof": { @@ -8512,7 +8512,7 @@ "spawn-sync": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/spawn-sync/-/spawn-sync-1.0.15.tgz", - "integrity": "sha512-9DWBgrgYZzNghseho0JOuh+5fg9u6QWhAWa51QC7+U5rCheZ/j1DrEZnyE0RBBRqZ9uEXGPgSSM0nky6burpVw==", + "integrity": "sha1-sAeZVX63+wyDdsKdROih6mfldHY=", "dev": true, "requires": { "concat-stream": "^1.4.7", @@ -8572,13 +8572,13 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", "dev": true }, "stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", "dev": true }, "statuses": { @@ -8680,7 +8680,7 @@ "is-obj": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=", "dev": true } } @@ -8697,13 +8697,13 @@ "strip-bom": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", "dev": true }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, "strip-final-newline": { @@ -8808,7 +8808,7 @@ "tabtab": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/tabtab/-/tabtab-2.2.2.tgz", - "integrity": "sha512-xEwHn571JmOrNGJB1Ehu/Dc2/5pu4aIvCnlKmxrJzzhAmZEy8+RL5cjxq/J66GE0Qf8FRvFg9V3jFos8oz0IQA==", + "integrity": "sha1-egR/FDsBC0y9MfhX6ClhUSy/ThQ=", "dev": true, "requires": { "debug": "^2.2.0", @@ -8842,7 +8842,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true } } @@ -8850,7 +8850,7 @@ "taffydb": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==", + "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", "dev": true }, "temp-dir": { @@ -8904,13 +8904,13 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", "dev": true }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, "through2": { @@ -8944,7 +8944,7 @@ "to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", "dev": true }, "to-regex-range": { @@ -8989,13 +8989,13 @@ "tsmlb": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tsmlb/-/tsmlb-1.0.0.tgz", - "integrity": "sha512-FjFkSk95wsBE5x+HnfwTUMC88LTv+QwSiAH11OwwhIu01UjIZChnAxTUCSUiZ0B9Dks66CsK0CM8loqGhXDjgw==", + "integrity": "sha1-wnDimdh9pfAPmjJJvEJLUaQhTWc=", "dev": true }, "type-check": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", "dev": true, "requires": { "prelude-ls": "~1.1.2" @@ -9026,7 +9026,7 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, "ua-parser-js": { @@ -9097,7 +9097,7 @@ "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=", "dev": true }, "unzipper": { @@ -9120,7 +9120,7 @@ "bluebird": { "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", - "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=", "dev": true }, "readable-stream": { @@ -9168,7 +9168,7 @@ "update-section": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/update-section/-/update-section-0.3.3.tgz", - "integrity": "sha512-BpRZMZpgXLuTiKeiu7kK0nIPwGdyrqrs6EDSaXtjD/aQ2T+qVo9a5hRC3HN3iJjCMxNT/VxoLGQ7E/OzE5ucnw==", + "integrity": "sha1-RY8Xgg03gg3GDiC4bZQ5GwASMVg=", "dev": true }, "uri-js": { @@ -9188,13 +9188,13 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", "dev": true }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=", "dev": true }, "uuid": { @@ -9372,7 +9372,7 @@ "void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=", "dev": true }, "water-plant-uml": { @@ -9452,7 +9452,7 @@ "wordwrap": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", "dev": true }, "wrap-ansi": { @@ -9495,7 +9495,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, "ws": { diff --git a/package.json b/package.json index 399b83499..2db6c25a3 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "video.js": "^7 || ^8" }, "peerDependencies": { - "video.js": "^8.11.0" + "video.js": "^8.14.0" }, "devDependencies": { "@babel/cli": "^7.21.0", diff --git a/src/media-segment-request.js b/src/media-segment-request.js index 6337f42c7..2eabfb090 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -10,6 +10,7 @@ import { } from '@videojs/vhs-utils/es/containers'; import {merge} from './util/vjs-compat'; import { getStreamingNetworkErrorMetadata } from './error-codes.js'; +import { getSegmentInfoFromSimpleSegment } from './segment-loader.js'; export const REQUEST_ERRORS = { FAILURE: 2, @@ -159,7 +160,7 @@ const handleKeyResponse = (segment, objects, finishProcessingFn, triggerSegmentE } const keyInfo = { uri: request.uri }; - triggerSegmentEventFn({ type: 'segmentkeyloadcomplete', keyInfo }); + triggerSegmentEventFn({ type: 'segmentkeyloadcomplete', segment, keyInfo }); return finishProcessingFn(null, segment); }; @@ -177,7 +178,6 @@ const parseInitSegment = (segment, callback) => { message: `Found unsupported ${mediaType} container for initialization segment at URL: ${uri}`, code: REQUEST_ERRORS.FAILURE, metadata: { - errorType: videojs.Error.UnsupportedMediaInitialization, mediaType } }); @@ -230,7 +230,7 @@ const handleInitSegmentResponse = } const bytes = new Uint8Array(request.response); - triggerSegmentEventFn({ type: 'segmentloaded', isInit: true }); + triggerSegmentEventFn({ type: 'segmentloaded', segment }); // init segment is encypted, we will have to wait // until the key request is done to decrypt. if (segment.map.key) { @@ -273,7 +273,7 @@ const handleSegmentResponse = ({ if (errorObj) { return finishProcessingFn(errorObj, segment); } - triggerSegmentEventFn({ type: 'segmentloaded' }); + triggerSegmentEventFn({ type: 'segmentloaded', segment }); const newBytes = // although responseText "should" exist, this guard serves to prevent an error being // thrown for two primary cases: @@ -343,7 +343,7 @@ const transmuxAndNotify = ({ hasVideo: trackInfo.hasVideo }; - triggerSegmentEventFn({ type: 'segmenttransmuxingtrackinfoavailable', trackInfo: info }); + triggerSegmentEventFn({ type: 'segmenttransmuxingtrackinfoavailable', segment, trackInfo: info }); } }, onAudioTimingInfo: (audioTimingInfo) => { @@ -380,7 +380,7 @@ const transmuxAndNotify = ({ } }; - triggerSegmentEventFn({ type: 'segmenttransmuxingtiminginfoavailable', timingInfo }); + triggerSegmentEventFn({ type: 'segmenttransmuxingtiminginfoavailable', segment, timingInfo }); videoSegmentTimingInfoFn(videoSegmentTimingInfo); }, onAudioSegmentTimingInfo: (audioSegmentTimingInfo) => { @@ -395,7 +395,7 @@ const transmuxAndNotify = ({ } }; - triggerSegmentEventFn({ type: 'segmenttransmuxingtiminginfoavailable', timingInfo }); + triggerSegmentEventFn({ type: 'segmenttransmuxingtiminginfoavailable', segment, timingInfo }); audioSegmentTimingInfoFn(audioSegmentTimingInfo); }, onId3: (id3Frames, dispatchType) => { @@ -409,14 +409,15 @@ const transmuxAndNotify = ({ endedTimelineFn(); }, onTransmuxerLog, - onDone: (result) => { + onDone: (result, error) => { if (!doneFn) { return; } result.type = result.type === 'combined' ? 'video' : result.type; - triggerSegmentEventFn({ type: 'segmenttransmuxingcomplete' }); - doneFn(null, segment, result); + triggerSegmentEventFn({ type: 'segmenttransmuxingcomplete', segment }); + doneFn(error, segment, result); }, + segment, triggerSegmentEventFn }); @@ -616,7 +617,8 @@ const handleSegmentBytes = ({ }); }; -const decrypt = function({id, key, encryptedBytes, decryptionWorker}, callback) { +const decrypt = function({ segment, decryptionWorker, doneFn }, callback) { + const { id, key, encryptedBytes } = segment; const decryptionHandler = (event) => { if (event.data.source === id) { decryptionWorker.removeEventListener('message', decryptionHandler); @@ -632,7 +634,21 @@ const decrypt = function({id, key, encryptedBytes, decryptionWorker}, callback) decryptionWorker.addEventListener('message', decryptionHandler); decryptionWorker.addEventListener('error', () => { - // call StreamingFailedToDecryptSegmentError here. + const message = 'An error occurred in the decryption worker'; + const segmentInfo = getSegmentInfoFromSimpleSegment(segment); + const decryptError = { + message, + metadata: { + error: new Error(message), + errorType: videojs.Error.StreamingFailedToDecryptSegment, + segmentInfo, + keyInfo: { + uri: segment.key.resolvedUri || segment.map.key.resolvedUri + } + } + }; + + doneFn(decryptError, segment); }); let keyBytes; @@ -696,14 +712,12 @@ const decryptSegment = ({ }) => { triggerSegmentEventFn({ type: 'segmentdecryptionstart' }); decrypt({ - id: segment.requestId, - key: segment.key, - encryptedBytes: segment.encryptedBytes, + segment, decryptionWorker, - triggerSegmentEventFn + doneFn }, (decryptedBytes) => { segment.bytes = decryptedBytes; - triggerSegmentEventFn({ type: 'segmentdecryptioncomplete' }); + triggerSegmentEventFn({ type: 'segmentdecryptioncomplete', segment }); handleSegmentBytes({ segment, @@ -840,19 +854,17 @@ const waitForCompletion = ({ // Keep track of when *all* of the requests have completed segment.endOfAllRequests = Date.now(); if (segment.map && segment.map.encryptedBytes && !segment.map.bytes) { - triggerSegmentEventFn({ type: 'segmentdecryptionstart', isInit: true }); + triggerSegmentEventFn({ type: 'segmentdecryptionstart', segment }); + // add -init to the "id" to differentiate between segment + // and init segment decryption, just in case they happen + // at the same time at some point in the future. + segment.requestId += '-init'; return decrypt({ decryptionWorker, - // add -init to the "id" to differentiate between segment - // and init segment decryption, just in case they happen - // at the same time at some point in the future. - id: segment.requestId + '-init', - encryptedBytes: segment.map.encryptedBytes, - key: segment.map.key, - triggerSegmentEventFn + segment }, (decryptedBytes) => { segment.map.bytes = decryptedBytes; - triggerSegmentEventFn({ type: 'segmentdecryptioncomplete', isInit: true }); + triggerSegmentEventFn({ type: 'segmentdecryptioncomplete', segment }); parseInitSegment(segment, (parseError) => { if (parseError) { abortAll(activeXhrs); @@ -1061,7 +1073,7 @@ export const mediaSegmentRequest = ({ const keyRequestCallback = handleKeyResponse(segment, objects, finishProcessingFn, triggerSegmentEventFn); const keyInfo = { uri: segment.key.resolvedUri }; - triggerSegmentEventFn({ type: 'segmentkeyloadstart', keyInfo }); + triggerSegmentEventFn({ type: 'segmentkeyloadstart', segment, keyInfo }); const keyXhr = xhr(keyRequestOptions, keyRequestCallback); activeXhrs.push(keyXhr); @@ -1080,7 +1092,7 @@ export const mediaSegmentRequest = ({ const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn, triggerSegmentEventFn); const keyInfo = { uri: segment.key.resolvedUri }; - triggerSegmentEventFn({ type: 'segmentkeyloadstart', keyInfo }); + triggerSegmentEventFn({ type: 'segmentkeyloadstart', segment, keyInfo }); const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback); activeXhrs.push(mapKeyXhr); @@ -1093,7 +1105,7 @@ export const mediaSegmentRequest = ({ }); const initSegmentRequestCallback = handleInitSegmentResponse({segment, finishProcessingFn, triggerSegmentEventFn}); - triggerSegmentEventFn({ type: 'segmentloadstart', isInit: true }); + triggerSegmentEventFn({ type: 'segmentloadstart', segment }); const initSegmentXhr = xhr(initSegmentOptions, initSegmentRequestCallback); activeXhrs.push(initSegmentXhr); @@ -1113,7 +1125,7 @@ export const mediaSegmentRequest = ({ triggerSegmentEventFn }); - triggerSegmentEventFn({ type: 'segmentloadstart' }); + triggerSegmentEventFn({ type: 'segmentloadstart', segment }); const segmentXhr = xhr(segmentRequestOptions, segmentRequestCallback); segmentXhr.addEventListener( diff --git a/src/playback-watcher.js b/src/playback-watcher.js index e8772990d..8f85d998c 100644 --- a/src/playback-watcher.js +++ b/src/playback-watcher.js @@ -11,7 +11,7 @@ import window from 'global/window'; import * as Ranges from './ranges'; import logger from './util/logger'; -import { createTimeRange } from 'video.js/dist/types/utils/time'; +import { createTimeRanges } from './util/vjs-compat'; // Set of events that reset the playback-watcher time check logic and clear the timeout const timerCancelEvents = [ @@ -278,7 +278,7 @@ export default class PlaybackWatcher { } else if (currentTime === this.lastRecordedTime) { this.consecutiveUpdates++; } else { - this.playedRanges_.push(createTimeRange(this.lastRecordedTime, currentTime)); + this.playedRanges_.push(createTimeRanges([this.lastRecordedTime, currentTime])); const metadata = { playedRanges: this.playedRanges_ }; diff --git a/src/segment-loader.js b/src/segment-loader.js index a72a49671..84f19d2c9 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -516,14 +516,30 @@ export const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => * @param {SegmentInfo} segmentInfo the full SegmentInfo object to be reduced. * @return the reduced payload. */ -const segmentInfoPayload = (type, segmentInfo, isInit = false) => { +export const getSegmentInfoFromSimpleSegment = (segment) => { + const { type, resolvedUri, start, duration, isEncrypted, isMediaInitialization } = segment; + + return { + type, + uri: resolvedUri, + start, + duration, + isEncrypted, + isMediaInitialization + }; +}; + +const segmentInfoPayload = (type, segmentInfo) => { + const isEncrypted = segmentInfo.segment.key || segmentInfo.segment.map && segmentInfo.segment.map.key; + const isMediaInitialization = segmentInfo.segment.map && !segmentInfo.segment.map.bytes; + return { type, uri: segmentInfo.uri, start: segmentInfo.startOfSegment, duration: segmentInfo.duration, - isEncrypted: Boolean(segmentInfo.segment.key), - isMediaInitialization: isInit + isEncrypted, + isMediaInitialization }; }; @@ -1402,6 +1418,7 @@ bufferedEnd: ${lastBufferedEnd(this.buffered_())} if (!segmentInfo) { return; } + const metadata = { segmentInfo: segmentInfoPayload(this.loaderType_, segmentInfo) }; @@ -2470,7 +2487,7 @@ Fetch At Buffer: ${this.fetchAtBuffer_} message: `${type} append of ${bytes.length}b failed for segment ` + `#${segmentInfo.mediaIndex} in playlist ${segmentInfo.playlist.id}`, metadata: { - errorType: videojs.Error.SegmentAppendError + errorType: videojs.Error.StreamingFailedToAppendSegment } }); this.trigger('appenderror'); @@ -2497,7 +2514,7 @@ Fetch At Buffer: ${this.fetchAtBuffer_} }); } const metadata = { - segmentInfo: segmentInfoPayload(this.loaderType_, segmentInfo, Boolean(initSegment)) + segmentInfo: segmentInfoPayload(this.loaderType_, segmentInfo) }; this.trigger({ type: 'segmentappendstart', metadata }); @@ -2669,8 +2686,8 @@ ${segmentInfoString(segmentInfo)}`); onTransmuxerLog: ({message, level, stream}) => { this.logger_(`${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}`); }, - triggerSegmentEventFn: ({ type, isInit, keyInfo, trackInfo, timingInfo }) => { - const segInfo = segmentInfoPayload(this.loaderType_, segmentInfo, isInit); + triggerSegmentEventFn: ({ type, segment, keyInfo, trackInfo, timingInfo }) => { + const segInfo = getSegmentInfoFromSimpleSegment(segment); const metadata = { segmentInfo: segInfo, keyInfo, trackInfo, timingInfo }; this.trigger({ type, metadata }); @@ -2716,6 +2733,8 @@ ${segmentInfoString(segmentInfo)}`); createSimplifiedSegmentObj_(segmentInfo) { const segment = segmentInfo.segment; const part = segmentInfo.part; + const isEncrypted = segmentInfo.segment.key || segmentInfo.segment.map && segmentInfo.segment.map.key; + const isMediaInitialization = segmentInfo.segment.map && !segmentInfo.segment.map.bytes; const simpleSegment = { resolvedUri: part ? part.resolvedUri : segment.resolvedUri, @@ -2724,7 +2743,12 @@ ${segmentInfoString(segmentInfo)}`); transmuxer: segmentInfo.transmuxer, audioAppendStart: segmentInfo.audioAppendStart, gopsToAlignWith: segmentInfo.gopsToAlignWith, - part: segmentInfo.part + part: segmentInfo.part, + type: this.loaderType_, + start: segmentInfo.startOfSegment, + duration: segmentInfo.duration, + isEncrypted, + isMediaInitialization }; const previousSegment = segmentInfo.playlist.segments[segmentInfo.mediaIndex - 1]; diff --git a/src/segment-transmuxer.js b/src/segment-transmuxer.js index c57398da5..0fbec7e21 100644 --- a/src/segment-transmuxer.js +++ b/src/segment-transmuxer.js @@ -1,4 +1,6 @@ import TransmuxWorker from 'worker!./transmuxer-worker.js'; +import videojs from 'video.js'; +import { getSegmentInfoFromSimpleSegment } from './segment-loader'; export const handleData_ = (event, transmuxedData, callback) => { const { @@ -83,6 +85,7 @@ export const processTransmux = (options) => { onEndedTimeline, onTransmuxerLog, isEndOfTimeline, + segment, triggerSegmentEventFn } = options; const transmuxedData = { @@ -154,8 +157,20 @@ export const processTransmux = (options) => { dequeue(transmuxer); /* eslint-enable */ }; + const handleError = () => { + const error = { + message: 'Received an error message from the transmuxer worker', + metadata: { + errorType: videojs.Error.StreamingFailedToTransmuxSegment, + segmentInfo: getSegmentInfoFromSimpleSegment(segment) + } + }; + + onDone(null, error); + }; transmuxer.onmessage = handleMessage; + transmuxer.onerror = handleError; if (audioAppendStart) { transmuxer.postMessage({ @@ -183,7 +198,7 @@ export const processTransmux = (options) => { const buffer = bytes instanceof ArrayBuffer ? bytes : bytes.buffer; const byteOffset = bytes instanceof ArrayBuffer ? 0 : bytes.byteOffset; - triggerSegmentEventFn('segmenttransmuxingstart'); + triggerSegmentEventFn({ type: 'segmenttransmuxingstart', segment }); transmuxer.postMessage( { action: 'push', diff --git a/src/source-updater.js b/src/source-updater.js index 28bd7862e..853510701 100644 --- a/src/source-updater.js +++ b/src/source-updater.js @@ -299,6 +299,11 @@ const actions = { sourceBuffer.changeType(mime); sourceUpdater.codecs[type] = codec; } catch (e) { + metadata.errorType = videojs.Error.StreamingCodecsChangeError; + metadata.error = e; + e.metadata = metadata; + sourceUpdater.error_ = e; + sourceUpdater.trigger('error'); videojs.log.warn(`Failed to changeType on ${type}Buffer`, e); } } From bdc12d3a1f9212b79e3d18aa8e478f10b736cbc1 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Mon, 6 May 2024 20:15:53 -0700 Subject: [PATCH 04/14] fix: remove old errors --- src/segment-loader.js | 15 +++------------ src/videojs-http-streaming.js | 5 +---- src/vtt-segment-loader.js | 5 +---- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/src/segment-loader.js b/src/segment-loader.js index 84f19d2c9..2bef4dcff 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -2419,10 +2419,7 @@ Fetch At Buffer: ${this.fetchAtBuffer_} `video buffer: ${timeRangesToArray(videoBuffered).join(', ')}, `); this.error({ message: 'Quota exceeded error with append of a single segment of content', - excludeUntil: Infinity, - metadata: { - errorType: videojs.Error.SegmentExceedsSourceBufferQuota - } + excludeUntil: Infinity }); this.trigger('error'); return; @@ -2996,10 +2993,7 @@ ${segmentInfoString(segmentInfo)}`); if (!trackInfo) { this.error({ message: 'No starting media returned, likely due to an unsupported media format.', - playlistExclusionDuration: Infinity, - metadata: { - errorType: videojs.Error.SegmentUnsupportedMediaFormat - } + playlistExclusionDuration: Infinity }); this.trigger('error'); return; @@ -3080,10 +3074,7 @@ ${segmentInfoString(segmentInfo)}`); if (illegalMediaSwitchError) { this.error({ message: illegalMediaSwitchError, - playlistExclusionDuration: Infinity, - metadata: { - errorType: videojs.Error.SegmentSwitchError - } + playlistExclusionDuration: Infinity }); this.trigger('error'); return true; diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index df14c4ea2..c0f95e001 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -1096,10 +1096,7 @@ class VhsHandler extends Component { this.logger_('error while creating EME key session', err); this.player_.error({ message: 'Failed to initialize media keys for EME', - code: 3, - metadata: { - errorType: videojs.Error.EMEKeySessionCreationError - } + code: 3 }); }); } diff --git a/src/vtt-segment-loader.js b/src/vtt-segment-loader.js index bdc0bed15..c727088a0 100644 --- a/src/vtt-segment-loader.js +++ b/src/vtt-segment-loader.js @@ -313,10 +313,7 @@ export default class VTTSegmentLoader extends SegmentLoader { .then( () => this.segmentRequestFinished_(error, simpleSegment, result), () => this.stopForError({ - message: 'Error loading vtt.js', - metadata: { - errorType: videojs.Error.VttLoadError - } + message: 'Error loading vtt.js' }) ); return; From 56b625f2e66ec18f1826abdf501dc1239853bea1 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Tue, 7 May 2024 01:42:29 -0700 Subject: [PATCH 05/14] fix: event propogation --- src/dash-playlist-loader.js | 24 ++++++++-- src/playback-watcher.js | 11 ++++- src/playlist-controller.js | 83 +++++++++++++++++++++++++++++++---- src/playlist-loader.js | 28 ++++++++++-- src/rendition-mixin.js | 9 ++-- src/segment-loader.js | 6 ++- src/videojs-http-streaming.js | 56 +++++++++++++++++++++++ 7 files changed, 195 insertions(+), 22 deletions(-) diff --git a/src/dash-playlist-loader.js b/src/dash-playlist-loader.js index 24b445776..c04388bf4 100644 --- a/src/dash-playlist-loader.js +++ b/src/dash-playlist-loader.js @@ -824,9 +824,27 @@ export default class DashPlaylistLoader extends EventTarget { } this.addEventStreamToMetadataTrack_(newMain); - metadata.parsedManifest = newMain; - // TODO: Do we want to pass the entire parsed manifest here or just select parts? - this.trigger({type: 'manifestparsecomplete', metadata}); + if (newMain) { + const { duration, endList } = newMain; + const renditions = []; + + newMain.playlists.forEach((playlist) => { + renditions.push({ + id: playlist.id, + bandwidth: playlist.attributes.BANDWIDTH, + resolution: playlist.attributes.RESOLUTION, + codecs: playlist.attributes.CODECS + }); + }); + const parsedManifest = { + duration, + isLive: !endList, + renditions + }; + + metadata.parsedManifest = parsedManifest; + this.trigger({type: 'manifestparsecomplete', metadata}); + } return Boolean(newMain); } diff --git a/src/playback-watcher.js b/src/playback-watcher.js index 8f85d998c..9db9c95cd 100644 --- a/src/playback-watcher.js +++ b/src/playback-watcher.js @@ -12,6 +12,7 @@ import window from 'global/window'; import * as Ranges from './ranges'; import logger from './util/logger'; import { createTimeRanges } from './util/vjs-compat'; +import videojs from 'video.js'; // Set of events that reset the playback-watcher time check logic and clear the timeout const timerCancelEvents = [ @@ -25,7 +26,7 @@ const timerCancelEvents = [ /** * @class PlaybackWatcher */ -export default class PlaybackWatcher { +export default class PlaybackWatcher extends videojs.EventTarget { /** * Represents an PlaybackWatcher object. * @@ -33,6 +34,7 @@ export default class PlaybackWatcher { * @param {Object} options an object that includes the tech and settings */ constructor(options) { + super(); this.playlistController_ = options.playlistController; this.tech_ = options.tech; this.seekable = options.seekable; @@ -602,7 +604,14 @@ export default class PlaybackWatcher { // only seek if we still have not played this.tech_.setCurrentTime(nextRange.start(0) + Ranges.TIME_FUDGE_FACTOR); + const metadata = { + gapInfo: { + from: currentTime, + to: nextRange.start(0) + } + }; + this.playlistController_.trigger({type: 'gapjumped', metadata}); this.tech_.trigger({type: 'usage', name: 'vhs-gap-skip'}); } diff --git a/src/playlist-controller.js b/src/playlist-controller.js index fa683d61c..2118dc482 100644 --- a/src/playlist-controller.js +++ b/src/playlist-controller.js @@ -413,16 +413,14 @@ export class PlaylistController extends videojs.EventTarget { renditionInfo: { id: newId, bandwidth: playlist.attributes.BANDWIDTH, - resolution: { - width: playlist.attributes.RESOLUTION.width, - height: playlist.attributes.RESOLUTION.height - }, + resolution: playlist.attributes.RESOLUTION, codecs: playlist.attributes.CODECS }, cause }; - this.tech_.trigger({type: 'usage', name: `vhs-rendition-change-${cause}`, metadata}); + this.trigger({type: 'renditionselected', metadata}); + this.tech_.trigger({type: 'usage', name: `vhs-rendition-change-${cause}`}); } this.mainPlaylistLoader_.media(playlist, delay); } @@ -740,10 +738,32 @@ export class PlaylistController extends videojs.EventTarget { }); this.mainPlaylistLoader_.on('renditiondisabled', (metadata) => { - this.tech_.trigger({type: 'usage', name: 'vhs-rendition-disabled', metadata}); + // eslint-disable-next-line + this.trigger({...metadata}); + this.tech_.trigger({type: 'usage', name: 'vhs-rendition-disabled'}); }); this.mainPlaylistLoader_.on('renditionenabled', (metadata) => { - this.tech_.trigger({type: 'usage', name: 'vhs-rendition-enabled', metadata}); + this.trigger({...metadata}); + this.tech_.trigger({type: 'usage', name: 'vhs-rendition-enabled'}); + }); + + const playlistLoaderEvents = [ + 'manifestrequeststart', + 'manifestrequestcomplete', + 'manifestparsestart', + 'manifestparsecomplete', + 'playlistrequeststart', + 'playlistrequestcomplete', + 'playlistparsestart', + 'playlistparsecomplete', + 'renditiondisabled', + 'renditionenabled' + ]; + + playlistLoaderEvents.forEach((eventName) => { + this.mainPlaylistLoader_.on(eventName, (metadata) => { + this.trigger({...metadata}); + }); }); } @@ -963,6 +983,40 @@ export class PlaylistController extends videojs.EventTarget { this.logger_('audioSegmentLoader ended'); this.onEndOfStream(); }); + + const segmentLoaderEvents = [ + 'segmentselected', + 'segmentloadstart', + 'segmentloaded', + 'segmentkeyloadstart', + 'segmentkeyloadcomplete', + 'segmentdecryptionstart', + 'segmentdecryptioncomplete', + 'segmenttransmuxingstart', + 'segmenttransmuxingcomplete', + 'segmenttransmuxingtrackinfoavailable', + 'segmenttransmuxingtiminginfoavailable', + 'segmentappendstart', + 'appendsdone', + 'bandwidthupdated', + 'timelinechange', + 'codecschange' + ]; + + segmentLoaderEvents.forEach((eventName) => { + this.mainSegmentLoader_.on(eventName, (metadata) => { + this.trigger({...metadata}); + }); + + this.audioSegmentLoader_.on(eventName, (metadata) => { + this.trigger({...metadata}); + }); + + this.subtitleSegmentLoader_.on(eventName, (metadata) => { + this.trigger({...metadata}); + }); + }); + } mediaSecondsLoaded_() { @@ -1650,7 +1704,8 @@ export class PlaylistController extends videojs.EventTarget { seekableRanges: this.seekable_ }; - this.tech_.trigger({ type: 'seekablechanged', metadata }); + this.trigger({type: 'seekablerangeschanged', metadata}); + this.tech_.trigger('seekablechanged'); } /** @@ -2190,6 +2245,18 @@ export class PlaylistController extends videojs.EventTarget { */ attachContentSteeringListeners_() { this.contentSteeringController_.on('content-steering', this.excludeThenChangePathway_.bind(this)); + const contentSteeringEvents = [ + 'contentsteeringloadstart', + 'contentsteeringloadcomplete', + 'contentsteeringparsed' + ]; + + contentSteeringEvents.forEach((eventName) => { + this.contentSteeringController_.on(eventName, (metadata) => { + this.trigger({...metadata}); + }); + }); + if (this.sourceType_ === 'dash') { this.mainPlaylistLoader_.on('loadedplaylist', () => { const main = this.main(); diff --git a/src/playlist-loader.js b/src/playlist-loader.js index 0de5b898f..c25c6ded2 100644 --- a/src/playlist-loader.js +++ b/src/playlist-loader.js @@ -372,6 +372,28 @@ export const refreshDelay = (media, update) => { return (media.partTargetDuration || media.targetDuration || 10) * 500; }; +const playlistMetadataPayload = (playlists, type, isLive) => { + if (!playlists) { + return; + } + const renditions = []; + + playlists.forEach((playlist) => { + renditions.push({ + id: playlist.id, + bandwidth: playlist.attributes.BANDWIDTH, + resolution: playlist.attributes.RESOLUTION, + codecs: playlist.attributes.CODECS + }); + }); + + return { + type, + isLive, + renditions + }; +}; + /** * Load a playlist from a remote location * @@ -565,7 +587,7 @@ export default class PlaylistLoader extends EventTarget { } this.updateMediaUpdateTimeout_(refreshDelay(this.media(), !!update)); - metadata.parsedPlaylist = playlist; + metadata.parsedPlaylist = playlistMetadataPayload(this.main.playlists, metadata.playlistInfo.type, !this.media_.endList); this.trigger({ type: 'playlistparsecomplete', metadata }); this.trigger('loadedplaylist'); } @@ -908,8 +930,8 @@ export default class PlaylistLoader extends EventTarget { url: this.src }); - // TODO: Do we want to pass the entire parsed manifest here or just select fields? - metadata.parsedPlaylist = manifest; + // we haven't loaded any variant playlists here so we default to false for isLive. + metadata.parsedPlaylist = playlistMetadataPayload(manifest.playlists, metadata.type, false); this.trigger({ type: 'playlistparsecomplete', metadata }); this.setupInitialPlaylist(manifest); diff --git a/src/rendition-mixin.js b/src/rendition-mixin.js index e250264e6..8a2e35a60 100644 --- a/src/rendition-mixin.js +++ b/src/rendition-mixin.js @@ -31,10 +31,7 @@ const enableFunction = (loader, playlistID, changePlaylistFn) => (enable) => { renditionInfo: { id: playlistID, bandwidth: playlist.attributes.BANDWIDTH, - resolution: { - width: playlist.attributes.RESOLUTION.width, - height: playlist.attributes.RESOLUTION.height - }, + resolution: playlist.attributes.RESOLUTION, codecs: playlist.attributes.CODECS }, cause: 'fast-quality' @@ -44,9 +41,9 @@ const enableFunction = (loader, playlistID, changePlaylistFn) => (enable) => { // Ensure the outside world knows about our changes changePlaylistFn(playlist); if (enable) { - loader.trigger({ type: 'renditionenabled', metadata }); + loader.trigger({ type: 'renditionenabled', metadata}); } else { - loader.trigger({ type: 'renditiondisabled', metadata }); + loader.trigger({ type: 'renditiondisabled', metadata}); } } return enable; diff --git a/src/segment-loader.js b/src/segment-loader.js index 2bef4dcff..dd5d8607c 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -685,6 +685,9 @@ export default class SegmentLoader extends videojs.EventTarget { } }); + this.sourceUpdater_.on('codecschange', (metadata) => { + this.trigger({type: 'codecschange', metadata}); + }); // Only the main loader needs to listen for pending timeline changes, as the main // loader should wait for audio to be ready to change its timeline so that both main // and audio timelines change together. For more details, see the @@ -2815,7 +2818,8 @@ ${segmentInfoString(segmentInfo)}`); } }; - this.trigger({type: 'usage', name: 'vhs-bandwidth-update', metadata }); + // player event with payload + this.trigger({type: 'bandwidthupdated', metadata}); this.bandwidth = stats.bandwidth; this.roundTrip = stats.roundTripTime; diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index c0f95e001..6ec83940d 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -812,6 +812,7 @@ class VhsHandler extends Component { this.playbackWatcher_ = new PlaybackWatcher(playbackWatcherOptions); + this.attachStreamingEventListeners_(); this.playlistController_.on('error', () => { const player = videojs.players[this.tech_.options_.playerId]; let error = this.playlistController_.error; @@ -1323,6 +1324,61 @@ class VhsHandler extends Component { // This allows hooks to be set before the source is set to vhs when handleSource is called. this.player_.trigger('xhr-hooks-ready'); } + + attachStreamingEventListeners_() { + const playlistControllerEvents = [ + 'manifestrequeststart', + 'manifestrequestcomplete', + 'manifestparsestart', + 'manifestparsecomplete', + 'playlistrequeststart', + 'playlistrequestcomplete', + 'playlistparsestart', + 'playlistparsecomplete', + 'segmentselected', + 'segmentloadstart', + 'segmentloaded', + 'segmentkeyloadstart', + 'segmentkeyloadcomplete', + 'segmentdecryptionstart', + 'segmentdecryptioncomplete', + 'segmenttransmuxingstart', + 'segmenttransmuxingcomplete', + 'segmenttransmuxingtrackinfoavailable', + 'segmenttransmuxingtiminginfoavailable', + 'segmentappendstart', + 'appendsdone', + 'renditiondisabled', + 'renditionenabled', + 'renditionselected', + 'bandwidthupdated', + 'timelinechange', + 'codecschange', + 'seekablerangeschanged', + 'bufferedrangeschanged', + 'contentsteeringloadstart', + 'contentsteeringloadcomplete', + 'contentsteeringparsed' + ]; + + const playbackWatcher = [ + 'gapjumped', + 'playedrangeschanged' + ]; + + // re-emit streaming events and payloads on the player. + playlistControllerEvents.forEach((eventName) => { + this.playlistController_.on(eventName, (metadata) => { + this.player_.trigger({...metadata}); + }); + }); + + playbackWatcher.forEach((eventName) => { + this.playbackWatcher_.on(eventName, (metadata) => { + this.player_.trigger({...metadata}); + }); + }); + } } /** From 69c4fad803f721cb2cacf163cc9d6d981602ef7a Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Wed, 8 May 2024 11:28:27 -0700 Subject: [PATCH 06/14] fix: vjs version in lock --- package-lock.json | 67 +++++++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 40 deletions(-) diff --git a/package-lock.json b/package-lock.json index 32679789e..d8692cee7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1779,9 +1779,9 @@ } }, "@videojs/http-streaming": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.10.0.tgz", - "integrity": "sha512-Lf1rmhTalV4Gw0bJqHmH4lfk/FlepUDs9smuMtorblAYnqDlE2tbUOb7sBXVYoXGdbWbdTW8jH2cnS+6HWYJ4Q==", + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-3.12.2.tgz", + "integrity": "sha512-P7l3qZdxW216b6KWPBBr+7Sj95exL25AWyD+hJsHA/Ghwrh8FsKplMleCE6JBumVT+5on1efMAPAFBlarv9c2w==", "requires": { "@babel/runtime": "^7.12.5", "@videojs/vhs-utils": "4.0.0", @@ -1789,30 +1789,8 @@ "global": "^4.4.0", "m3u8-parser": "^7.1.0", "mpd-parser": "^1.3.0", - "mux.js": "7.0.2", + "mux.js": "7.0.3", "video.js": "^7 || ^8" - }, - "dependencies": { - "mpd-parser": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-1.3.0.tgz", - "integrity": "sha512-WgeIwxAqkmb9uTn4ClicXpEQYCEduDqRKfmUdp4X8vmghKfBNXZLYpREn9eqrDx/Tf5LhzRcJLSpi4ohfV742Q==", - "requires": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^4.0.0", - "@xmldom/xmldom": "^0.8.3", - "global": "^4.4.0" - } - }, - "mux.js": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/mux.js/-/mux.js-7.0.2.tgz", - "integrity": "sha512-CM6+QuyDbc0qW1OfEjkd2+jVKzTXF+z5VOKH0eZxtZtnrG/ilkW/U7l7IXGtBNLASF9sKZMcK1u669cq50Qq0A==", - "requires": { - "@babel/runtime": "^7.11.2", - "global": "^4.4.0" - } - } } }, "@videojs/update-changelog": { @@ -9226,12 +9204,12 @@ "dev": true }, "video.js": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.11.0.tgz", - "integrity": "sha512-wlcCwZztiWlNxhC1PzKTxWQv+OK+z+Cqyc/SI/kNnLkLQ3gr/Zh2rRikLMSMrw0QJD02aQWmQFXdlB2aiLzSzQ==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/video.js/-/video.js-8.14.0.tgz", + "integrity": "sha512-XoKvHAENYpmLI66GYkcGaGuAhHwYMcxWNO1XRRfVl9v6U62jC1PGVd7Fd/AnsOCTiPjYqzJIAOD4tYJTJtb0cA==", "requires": { "@babel/runtime": "^7.12.5", - "@videojs/http-streaming": "3.10.0", + "@videojs/http-streaming": "3.12.2", "@videojs/vhs-utils": "^4.0.0", "@videojs/xhr": "2.6.0", "aes-decrypter": "^4.0.1", @@ -9241,22 +9219,17 @@ "mpd-parser": "^1.2.2", "mux.js": "^7.0.1", "safe-json-parse": "4.0.0", - "videojs-contrib-quality-levels": "4.0.0", + "videojs-contrib-quality-levels": "4.1.0", "videojs-font": "4.1.0", "videojs-vtt.js": "0.15.5" }, "dependencies": { - "videojs-font": { + "videojs-contrib-quality-levels": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.1.0.tgz", - "integrity": "sha512-X1LuPfLZPisPLrANIAKCknZbZu5obVM/ylfd1CN+SsCmPZQ3UMDPcvLTpPBJxcBuTpHQq2MO1QCFt7p8spnZ/w==" - }, - "videojs-vtt.js": { - "version": "0.15.5", - "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", - "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.1.0.tgz", + "integrity": "sha512-TfrXJJg1Bv4t6TOCMEVMwF/CoS8iENYsWNKip8zfhB5kTcegiFYezEA0eHAJPU64ZC8NQbxQgOwAsYU8VXbOWA==", "requires": { - "global": "^4.3.1" + "global": "^4.4.0" } } } @@ -9275,10 +9248,16 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/videojs-contrib-quality-levels/-/videojs-contrib-quality-levels-4.0.0.tgz", "integrity": "sha512-u5rmd8BjLwANp7XwuQ0Q/me34bMe6zg9PQdHfTS7aXgiVRbNTb4djcmfG7aeSrkpZjg+XCLezFNenlJaCjBHKw==", + "dev": true, "requires": { "global": "^4.4.0" } }, + "videojs-font": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-4.1.0.tgz", + "integrity": "sha512-X1LuPfLZPisPLrANIAKCknZbZu5obVM/ylfd1CN+SsCmPZQ3UMDPcvLTpPBJxcBuTpHQq2MO1QCFt7p8spnZ/w==" + }, "videojs-generate-karma-config": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/videojs-generate-karma-config/-/videojs-generate-karma-config-8.0.1.tgz", @@ -9369,6 +9348,14 @@ } } }, + "videojs-vtt.js": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.5.tgz", + "integrity": "sha512-yZbBxvA7QMYn15Lr/ZfhhLPrNpI/RmCSCqgIff57GC2gIrV5YfyzLfLyZMj0NnZSAz8syB4N0nHXpZg9MyrMOQ==", + "requires": { + "global": "^4.3.1" + } + }, "void-elements": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", From 4ee1fd5b02bdb548755bbd07597ac4acb50a1f75 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Thu, 9 May 2024 11:43:10 -0700 Subject: [PATCH 07/14] fix: existing tests --- src/dash-playlist-loader.js | 10 ++--- src/error-codes.js | 15 ++++--- src/media-segment-request.js | 30 ++++++++----- src/playlist-loader.js | 14 ++++-- src/segment-loader.js | 3 ++ src/util/container-request.js | 4 +- test/dash-playlist-loader.test.js | 12 ++--- test/media-segment-request.test.js | 70 +++++++++++++++++++---------- test/segment-loader.test.js | 7 +-- test/segment-transmuxer.test.js | 15 ++++--- test/videojs-http-streaming.test.js | 5 +-- 11 files changed, 114 insertions(+), 71 deletions(-) diff --git a/src/dash-playlist-loader.js b/src/dash-playlist-loader.js index c04388bf4..6d988690a 100644 --- a/src/dash-playlist-loader.js +++ b/src/dash-playlist-loader.js @@ -392,17 +392,12 @@ export default class DashPlaylistLoader extends EventTarget { const uri = resolveManifestRedirect(playlist.sidx.resolvedUri); const fin = (err, request) => { - const { requestType } = request; - - if (err) { - err.metadata = getStreamingNetworkErrorMetadata({ requestType, request, error: err }); - } - if (this.requestErrored_(err, request, startingState)) { return; } const sidxMapping = this.mainPlaylistLoader_.sidxMapping_; + const { requestType } = request; let sidx; try { @@ -424,6 +419,7 @@ export default class DashPlaylistLoader extends EventTarget { return cb(true); }; + const REQUEST_TYPE = 'dash-sidx'; this.request = containerRequest(uri, this.vhs_.xhr, (err, request, container, bytes) => { if (err) { @@ -465,7 +461,7 @@ export default class DashPlaylistLoader extends EventTarget { requestType: 'dash-sidx', headers: segmentXhrHeaders({byterange: playlist.sidx.byterange}) }, fin); - }); + }, REQUEST_TYPE); } dispose() { diff --git a/src/error-codes.js b/src/error-codes.js index dc2b41df5..2a3ae6beb 100644 --- a/src/error-codes.js +++ b/src/error-codes.js @@ -5,19 +5,22 @@ export const QUOTA_EXCEEDED_ERR = 22; export const getStreamingNetworkErrorMetadata = ({ requestType, request, error, parseFailure }) => { const isBadStatus = request.status < 200 || request.status > 299; + const isFailure = request.status >= 400 && request.status <= 499; const errorMetadata = { uri: request.uri, requestType }; + const isBadStatusOrParseFailure = (isBadStatus && !isFailure) || parseFailure; - if (error) { - errorMetadata.error = error; + if (error && isFailure) { + // copy original error and add to the metadata. + errorMetadata.error = {...error}; errorMetadata.errorType = videojs.Error.NetworkRequestFailed; - } else if (request.timedout) { - errorMetadata.errorType = videojs.Error.NetworkRequestTimeout; } else if (request.aborted) { - errorMetadata.erroType = videojs.Error.NetworkRequestAborted; - } else if (parseFailure || isBadStatus) { + errorMetadata.errorType = videojs.Error.NetworkRequestAborted; + } else if (request.timedout) { + errorMetadata.erroType = videojs.Error.NetworkRequestTimeout; + } else if (isBadStatusOrParseFailure) { const errorType = parseFailure ? videojs.Error.NetworkBodyParserFailed : videojs.Error.NetworkBadStatus; errorMetadata.errorType = errorType; diff --git a/src/media-segment-request.js b/src/media-segment-request.js index 2eabfb090..82ed08988 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -617,8 +617,7 @@ const handleSegmentBytes = ({ }); }; -const decrypt = function({ segment, decryptionWorker, doneFn }, callback) { - const { id, key, encryptedBytes } = segment; +const decrypt = function({id, key, encryptedBytes, decryptionWorker, segment, doneFn}, callback) { const decryptionHandler = (event) => { if (event.data.source === id) { decryptionWorker.removeEventListener('message', decryptionHandler); @@ -632,8 +631,7 @@ const decrypt = function({ segment, decryptionWorker, doneFn }, callback) { } }; - decryptionWorker.addEventListener('message', decryptionHandler); - decryptionWorker.addEventListener('error', () => { + decryptionWorker.onerror = () => { const message = 'An error occurred in the decryption worker'; const segmentInfo = getSegmentInfoFromSimpleSegment(segment); const decryptError = { @@ -649,7 +647,9 @@ const decrypt = function({ segment, decryptionWorker, doneFn }, callback) { }; doneFn(decryptError, segment); - }); + }; + + decryptionWorker.addEventListener('message', decryptionHandler); let keyBytes; if (key.bytes.slice) { @@ -712,8 +712,11 @@ const decryptSegment = ({ }) => { triggerSegmentEventFn({ type: 'segmentdecryptionstart' }); decrypt({ - segment, + id: segment.requestId, + key: segment.key, + encryptedBytes: segment.encryptedBytes, decryptionWorker, + segment, doneFn }, (decryptedBytes) => { segment.bytes = decryptedBytes; @@ -859,10 +862,15 @@ const waitForCompletion = ({ // and init segment decryption, just in case they happen // at the same time at some point in the future. segment.requestId += '-init'; - return decrypt({ - decryptionWorker, - segment - }, (decryptedBytes) => { + return decrypt({ decryptionWorker, + // add -init to the "id" to differentiate between segment + // and init segment decryption, just in case they happen + // at the same time at some point in the future. + id: segment.requestId + '-init', + encryptedBytes: segment.map.encryptedBytes, + key: segment.map.key, + segment, + doneFn }, (decryptedBytes) => { segment.map.bytes = decryptedBytes; triggerSegmentEventFn({ type: 'segmentdecryptioncomplete', segment }); parseInitSegment(segment, (parseError) => { @@ -1090,7 +1098,7 @@ export const mediaSegmentRequest = ({ requestType: 'segment-key' }); const mapKeyRequestCallback = handleKeyResponse(segment, [segment.map.key], finishProcessingFn, triggerSegmentEventFn); - const keyInfo = { uri: segment.key.resolvedUri }; + const keyInfo = { uri: segment.map.key.resolvedUri }; triggerSegmentEventFn({ type: 'segmentkeyloadstart', segment, keyInfo }); const mapKeyXhr = xhr(mapKeyRequestOptions, mapKeyRequestCallback); diff --git a/src/playlist-loader.js b/src/playlist-loader.js index c25c6ded2..e232e7215 100644 --- a/src/playlist-loader.js +++ b/src/playlist-loader.js @@ -379,11 +379,17 @@ const playlistMetadataPayload = (playlists, type, isLive) => { const renditions = []; playlists.forEach((playlist) => { + // we need attributes to populate rendition data. + if (!playlist.attributes) { + return; + } + const { BANDWIDTH, RESOLUTION, CODECS } = playlist.attributes; + renditions.push({ id: playlist.id, - bandwidth: playlist.attributes.BANDWIDTH, - resolution: playlist.attributes.RESOLUTION, - codecs: playlist.attributes.CODECS + bandwidth: BANDWIDTH, + resolution: RESOLUTION, + codecs: CODECS }); }); @@ -509,7 +515,7 @@ export default class PlaylistLoader extends EventTarget { message: `HLS playlist request error at URL: ${uri}.`, responseText: xhr.responseText, code: (xhr.status >= 500) ? 4 : 2, - metadata: getStreamingNetworkErrorMetadata({ requestType: xhr.requestType, xhr, error: xhr.error }) + metadata: getStreamingNetworkErrorMetadata({ requestType: xhr.requestType, request: xhr, error: xhr.error }) }; this.trigger('error'); diff --git a/src/segment-loader.js b/src/segment-loader.js index dd5d8607c..4fbc61546 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -517,6 +517,9 @@ export const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => * @return the reduced payload. */ export const getSegmentInfoFromSimpleSegment = (segment) => { + if (!segment) { + return; + } const { type, resolvedUri, start, duration, isEncrypted, isMediaInitialization } = segment; return { diff --git a/src/util/container-request.js b/src/util/container-request.js index 8bd571936..affd31b5e 100644 --- a/src/util/container-request.js +++ b/src/util/container-request.js @@ -2,6 +2,7 @@ import {getId3Offset} from '@videojs/vhs-utils/es/id3-helpers'; import {detectContainerForBytes} from '@videojs/vhs-utils/es/containers'; import {stringToBytes, concatTypedArrays} from '@videojs/vhs-utils/es/byte-helpers'; import {callbackWrapper} from '../xhr'; +import { getStreamingNetworkErrorMetadata } from '../error-codes'; // calls back if the request is readyState DONE // which will only happen if the request is complete. @@ -12,7 +13,7 @@ const callbackOnCompleted = (request, cb) => { return; }; -const containerRequest = (uri, xhr, cb) => { +const containerRequest = (uri, xhr, cb, requestType) => { let bytes = []; let id3Offset; let finished = false; @@ -28,6 +29,7 @@ const containerRequest = (uri, xhr, cb) => { return; } if (error) { + error.metadata = getStreamingNetworkErrorMetadata({ requestType, request, error }); return endRequestAndCallback(error, request, '', bytes); } diff --git a/test/dash-playlist-loader.test.js b/test/dash-playlist-loader.test.js index aa32d2755..0b91545a9 100644 --- a/test/dash-playlist-loader.test.js +++ b/test/dash-playlist-loader.test.js @@ -723,10 +723,6 @@ QUnit.test('addSidxSegments_: adds/triggers error on invalid container', functio code: 2, internal: true, message: 'Unsupported unknown container type for sidx segment at URL: sidx.mp4', - metadata: { - errorType: 'unsupported-sidx-container-error', - sidxContainer: 'unknown' - }, playlist, response: '', status: 200 @@ -2026,7 +2022,13 @@ QUnit.test('addSidxSegments_: errors if request for sidx fails', function(assert { status: 500, message: 'DASH request error at URL: sidx.mp4', - metadata: undefined, + metadata: { + errorType: 'networkbadstatus', + headers: {}, + requestType: 'dash-sidx', + status: 500, + uri: 'sidx.mp4' + }, response: '', code: 2 }, diff --git a/test/media-segment-request.test.js b/test/media-segment-request.test.js index bfb89843e..6943d3e35 100644 --- a/test/media-segment-request.test.js +++ b/test/media-segment-request.test.js @@ -97,7 +97,8 @@ QUnit.module('Media Segment Request - make it to transmuxer', { xhrOptions: this.xhrOptions, decryptionWorker: this.mockDecrypter, segment: {}, - onTransmuxerLog: () => {} + onTransmuxerLog: () => {}, + triggerSegmentEventFn: () => {} }; [ @@ -303,7 +304,8 @@ QUnit.test('cancels outstanding segment request on abort', function(assert) { segment: { resolvedUri: '0-test.ts' }, abortFn: () => aborts++, progressFn: this.noop, - doneFn: this.noop + doneFn: this.noop, + triggerSegmentEventFn: this.noop }); // Simulate Firefox's handling of aborted segments - @@ -335,7 +337,8 @@ QUnit.test('cancels outstanding key requests on abort', function(assert) { }, abortFn: () => aborts++, progressFn: this.noop, - doneFn: this.noop + doneFn: this.noop, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -378,7 +381,8 @@ QUnit.test('cancels outstanding key requests on failure', function(assert) { assert.equal(error.code, REQUEST_ERRORS.FAILURE, 'segment request failed'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -414,7 +418,8 @@ QUnit.test('cancels outstanding key requests on timeout', function(assert) { assert.equal(error.code, REQUEST_ERRORS.TIMEOUT, 'key request failed'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -453,7 +458,8 @@ QUnit.test( assert.equal(error.code, REQUEST_ERRORS.FAILURE, 'request failed'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -474,6 +480,7 @@ QUnit.test( } ); +// TODO: come back to this QUnit.test('the key response is converted to the correct format', function(assert) { const done = assert.async(); const postMessage = this.mockDecrypter.postMessage; @@ -516,7 +523,8 @@ QUnit.test('the key response is converted to the correct format', function(asser // verify stats assert.equal(segmentData.stats.bytesReceived, 10, '10 bytes'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -563,7 +571,8 @@ QUnit.test('segment with key has bytes decrypted', function(assert) { // verify stats assert.equal(segmentData.stats.bytesReceived, 8, '8 bytes'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -613,7 +622,8 @@ QUnit.test('segment with key bytes does not request key again', function(assert) // verify stats assert.equal(segmentData.stats.bytesReceived, 8, '8 bytes'); done(); - }}); + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 1, 'there is one request'); const segmentReq = this.requests.shift(); @@ -654,7 +664,8 @@ QUnit.test('key 404 calls back with error', function(assert) { assert.equal(error.code, REQUEST_ERRORS.FAILURE, 'error code set to FAILURE'); assert.notOk(segmentData.bytes, 'no bytes in segment'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -697,7 +708,8 @@ QUnit.test('key 500 calls back with error', function(assert) { assert.equal(error.code, REQUEST_ERRORS.FAILURE, 'error code set to FAILURE'); assert.notOk(segmentData.bytes, 'no bytes in segment'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -775,7 +787,8 @@ QUnit.test('init segment with key has bytes decrypted', function(assert) { assert.ok(trackInfo, 'got track info'); assert.ok(Object.keys(timingInfo).length, 'got timing info'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 3, 'there are three requests'); @@ -881,7 +894,8 @@ QUnit.test('segment/init segment share a key and get decrypted', function(assert assert.ok(trackInfo, 'got track info'); assert.ok(Object.keys(timingInfo).length, 'got timing info'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 3, 'there are three requests'); @@ -987,7 +1001,8 @@ QUnit.test('segment/init segment different key and get decrypted', function(asse assert.ok(trackInfo, 'got track info'); assert.ok(Object.keys(timingInfo).length, 'got timing info'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 4, 'there are four requests'); @@ -1067,7 +1082,8 @@ QUnit.test('encrypted init segment parse error', function(assert) { // decrypted webm init segment caused this error. assert.ok(error, 'error for invalid init segment'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 4, 'there are four requests'); @@ -1133,7 +1149,8 @@ QUnit.test('encrypted init segment request failure', function(assert) { }); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 4, 'there are four requests'); @@ -1235,7 +1252,8 @@ QUnit.test('encrypted init segment with decrypted bytes not re-requested', funct assert.ok(trackInfo, 'got track info'); assert.ok(Object.keys(timingInfo).length, 'got timing info'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -1289,7 +1307,8 @@ QUnit.test( // verify stats assert.equal(segmentData.stats.bytesReceived, 8, '8 bytes'); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 3, 'there are three requests'); @@ -1404,7 +1423,8 @@ QUnit.test('non-TS segment will get parsed for captions', function(assert) { assert.ok(gotData, 'received data event'); transmuxer.off(); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -1451,7 +1471,8 @@ QUnit.test('webm segment calls back with error', function(assert) { 'receieved error message' ); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -1561,7 +1582,8 @@ QUnit.test('non-TS segment will get parsed for captions on next segment request assert.equal(gotData, 1, 'received data event'); transmuxer.off(); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -1692,7 +1714,8 @@ QUnit.test('can get emsg ID3 frames from fmp4 video segment', function(assert) { assert.equal(gotData, 1, 'received data event'); transmuxer.off(); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); @@ -1822,7 +1845,8 @@ QUnit.test('can get emsg ID3 frames from fmp4 audio segment', function(assert) { assert.equal(gotData, 1, 'received data event'); transmuxer.off(); done(); - } + }, + triggerSegmentEventFn: this.noop }); assert.equal(this.requests.length, 2, 'there are two requests'); diff --git a/test/segment-loader.test.js b/test/segment-loader.test.js index e424032cf..2dba67148 100644 --- a/test/segment-loader.test.js +++ b/test/segment-loader.test.js @@ -3147,7 +3147,7 @@ QUnit.module('SegmentLoader', function(hooks) { { message: 'video append of 2960b failed for segment #0 in playlist playlist.m3u8', metadata: { - errorType: 'segment-append-error' + errorType: 'streamingfailedtoappendsegment' } }, 'loader triggered and saved the appenderror' @@ -4818,10 +4818,7 @@ QUnit.module('SegmentLoader', function(hooks) { loader.error_, { message: 'Quota exceeded error with append of a single segment of content', - excludeUntil: Infinity, - metadata: { - errorType: 'segment-exceeds-source-buffer-quota-error' - } + excludeUntil: Infinity }, 'loader triggered and saved the error' ); diff --git a/test/segment-transmuxer.test.js b/test/segment-transmuxer.test.js index 9175aae5c..edfc4cdb4 100644 --- a/test/segment-transmuxer.test.js +++ b/test/segment-transmuxer.test.js @@ -84,7 +84,8 @@ QUnit.test('transmux returns data for full appends', function(assert) { assert.ok(videoSegmentTimingInfoFn.callCount, 'got videoSegmentTimingInfo events'); assert.ok(audioSegmentTimingInfoFn.callCount, 'got audioSegmentTimingInfo events'); done(); - } + }, + triggerSegmentEventFn: () => {} }); }); @@ -111,7 +112,8 @@ QUnit.test('transmux returns captions for full appends', function(assert) { assert.ok(dataFn.callCount, 'got data events'); assert.ok(captionsFn.callCount, 'got captions'); done(); - } + }, + triggerSegmentEventFn: () => {} }); }); @@ -221,7 +223,8 @@ QUnit.test('processTransmux posts all actions', function(assert) { onVideoTimingInfo: noop, onId3: noop, onCaptions: noop, - onDone: noop + onDone: noop, + triggerSegmentEventFn: () => {} }); assert.deepEqual( @@ -405,7 +408,8 @@ QUnit.test('transmux waits for endTimeline if isEndOfTimeline', function(assert) assert.ok(audioSegmentTimingInfoFn.callCount, 'got audioSegmentTimingInfo events'); assert.ok(onEndedTimelineFn.callCount, 'got onEndedTimeline event'); done(); - } + }, + triggerSegmentEventFn: () => {} }); }); @@ -445,6 +449,7 @@ QUnit.test('transmux does not wait for endTimeline if not isEndOfTimeline', func assert.ok(audioSegmentTimingInfoFn.callCount, 'got audioSegmentTimingInfo events'); assert.notOk(onEndedTimelineFn.callCount, 'did not get onEndedTimeline event'); done(); - } + }, + triggerSegmentEventFn: () => {} }); }); diff --git a/test/videojs-http-streaming.test.js b/test/videojs-http-streaming.test.js index f898bb006..929dc00b6 100644 --- a/test/videojs-http-streaming.test.js +++ b/test/videojs-http-streaming.test.js @@ -5149,10 +5149,7 @@ QUnit.test('player error when key session creation rejects promise', function(as errorObject, { code: 3, - message: 'Failed to initialize media keys for EME', - metadata: { - errorType: 'eme-key-session-creation-error' - } + message: 'Failed to initialize media keys for EME' }, 'called player error with correct error' ); From 1f1be5a1bf01e5d0bd89f9f8df8a938acda3528c Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Fri, 10 May 2024 09:17:15 -0700 Subject: [PATCH 08/14] fix: events, tests, linting add tests --- package-lock.json | 157 +++++++++++++++++-------------- package.json | 2 +- src/playlist-controller.js | 10 +- src/playlist-loader.js | 2 +- src/videojs-http-streaming.js | 29 +----- test/playback.test.js | 157 +++++++++++++++++++++++++++++++ test/playlist-controller.test.js | 55 +++++++---- 7 files changed, 289 insertions(+), 123 deletions(-) diff --git a/package-lock.json b/package-lock.json index d8692cee7..175dcace6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1384,9 +1384,9 @@ }, "dependencies": { "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -1846,7 +1846,7 @@ "JSV": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz", - "integrity": "sha1-0Hf2glVx+CEy+d/67Vh7QCn+/1c=", + "integrity": "sha512-ZJ6wx9xaKJ3yFUhq5/sk82PJMuUyLk277I8mQeyDgCTjGdjWJIvPfaU5LIXaMuaN2UO1X3kZH4+lgphublZUHw==", "dev": true }, "abbrev": { @@ -3783,9 +3783,9 @@ "dev": true }, "globals": { - "version": "13.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.12.0.tgz", - "integrity": "sha512-uS8X6lSKN2JumVoXrbUz+uG4BYG+eiawqm3qFcT7ammfbUHeCBoJMlHcec/S3krSk73/AE/f0szYFmgAA3kYZg==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -3808,9 +3808,9 @@ } }, "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", "dev": true, "requires": { "deep-is": "^0.1.3", @@ -3818,7 +3818,7 @@ "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "word-wrap": "^1.2.5" } }, "prelude-ls": { @@ -3850,13 +3850,19 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true + }, + "word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true } } }, "eslint-config-videojs": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-videojs/-/eslint-config-videojs-6.0.0.tgz", - "integrity": "sha512-kD/A8QGdmHH7SOkEidJt1ICN0B5d3yvAxMUXCWWWtJVf1jgGAwcWL43VSeYYwp31Iby77QgOOWtnvzuNvVhHpQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-videojs/-/eslint-config-videojs-6.1.0.tgz", + "integrity": "sha512-zEhT16DlA6Hz9NYhawEpqS0hmWbWjnK6egmgYpK76Q5F+ng9XS6M0TquYmLYuYoNrrFJu6l+cAjhW0ztt9RqkA==", "dev": true }, "eslint-plugin-jsdoc": { @@ -3886,9 +3892,9 @@ } }, "eslint-plugin-markdown": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-markdown/-/eslint-plugin-markdown-2.2.1.tgz", - "integrity": "sha512-FgWp4iyYvTFxPwfbxofTvXxgzPsDuSKHQy2S+a8Ve6savbujey+lgrFFbXQA0HPygISpRYWYBjooPzhYSF81iA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-markdown/-/eslint-plugin-markdown-3.0.1.tgz", + "integrity": "sha512-8rqoc148DWdGdmYF6WSQFT3uQ6PO7zXYgeBpHAOAakX/zpq+NvFYbDA/H7PYzHajwtmaOzAwfxyl++x0g1/N9A==", "dev": true, "requires": { "mdast-util-from-markdown": "^0.8.5" @@ -3967,9 +3973,9 @@ "dev": true }, "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", "dev": true, "requires": { "estraverse": "^5.1.0" @@ -4200,19 +4206,20 @@ } }, "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "requires": { - "flatted": "^3.1.0", + "flatted": "^3.2.9", + "keyv": "^4.5.3", "rimraf": "^3.0.2" }, "dependencies": { "flatted": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", - "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true } } @@ -4312,7 +4319,7 @@ "functional-red-black-tree": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", "dev": true }, "gauge": { @@ -4605,7 +4612,7 @@ "has-color": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", - "integrity": "sha1-ZxRKUmDDT8PMpnfQQdr1L+e3iy8=", + "integrity": "sha512-kaNz5OTAYYmt646Hkqw50/qyxP2vFnTVu5AQ1Zmk22Kk5+4Qx6BpO8+u7IKsML5fOsFk0ZT0AcCJNYwcvaLBvw==", "dev": true }, "has-flag": { @@ -4800,7 +4807,7 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, "indent-string": { @@ -5489,6 +5496,12 @@ "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -5510,7 +5523,7 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, "json-stringify-safe": { @@ -5915,6 +5928,15 @@ "resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz", "integrity": "sha512-ps3I9jAdNtRpJrbBvQjpzyFbss/skHqzS+eu4RxKLaEAtFqkjZaB6TZMSivPbLxf4K7VI4SjR0P5mRCX5+Q25A==" }, + "keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "requires": { + "json-buffer": "3.0.1" + } + }, "kind-of": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", @@ -6090,12 +6112,6 @@ "integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY=", "dev": true }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true - }, "lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -6147,7 +6163,7 @@ "lodash.truncate": { "version": "4.4.2", "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "dev": true }, "lodash.uniq": { @@ -6914,7 +6930,7 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, "negotiator": { @@ -6964,7 +6980,7 @@ "nomnom": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", - "integrity": "sha1-IVH3Ikcrp55Qp2/BJbuMjy5Nwqc=", + "integrity": "sha512-5s0JxqhDx9/rksG2BTMVN1enjWSvPidpoSgViZU4ZXULyTe+7jxcCRLB6f42Z0l1xYJpleCBtSyY6Lwg3uu5CQ==", "dev": true, "requires": { "chalk": "~0.4.0", @@ -6974,13 +6990,13 @@ "ansi-styles": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", - "integrity": "sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=", + "integrity": "sha512-3iF4FIKdxaVYT3JqQuY3Wat/T2t7TRbbQ94Fu50ZUCbLy4TFbTzr90NOHQodQkNqmeEGCw8WbeP78WNi6SKYUA==", "dev": true }, "chalk": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", - "integrity": "sha1-UZmj3c0MHv4jvAjBsCewYXbgxk8=", + "integrity": "sha512-sQfYDlfv2DGVtjdoQqxS0cEZDroyG8h6TamA6rvxwlrU5BaSLDx9xhatBYl2pxZ7gmpNaPFVwBtdGdu5rQ+tYQ==", "dev": true, "requires": { "ansi-styles": "~1.0.0", @@ -6991,13 +7007,7 @@ "strip-ansi": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", - "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", - "dev": true - }, - "underscore": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", - "integrity": "sha1-izixDKze9jM3uLJOT/htRa6lKag=", + "integrity": "sha512-behete+3uqxecWlDAm5lmskaSaISA+ThQ4oNNBDTBJt0x2ppR6IPqfZNuj6BLaLJ/Sji4TPZlcRyOis8wXQTLg==", "dev": true } } @@ -7526,9 +7536,9 @@ "dev": true }, "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "q": { @@ -8715,13 +8725,12 @@ } }, "table": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.2.tgz", - "integrity": "sha512-UFZK67uvyNivLeQbVtkiUs8Uuuxv24aSL4/Vil2PJVtMgU8Lx0CYkP12uCGa3kjyQzOSgV1+z9Wkb82fCGsO0g==", + "version": "6.8.2", + "resolved": "https://registry.npmjs.org/table/-/table-6.8.2.tgz", + "integrity": "sha512-w2sfv80nrAh2VCbqR5AK27wswXhqcck2AhfnNW76beQXskGZ1V12GwS//yYVa3d3fcvAip2OUnbDAjW2k3v9fA==", "dev": true, "requires": { "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", @@ -8729,15 +8738,15 @@ }, "dependencies": { "ajv": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.3.tgz", - "integrity": "sha512-SMJOdDP6LqTkD0Uq8qLi+gMwSt0imXLSV080qFVwJCpH9U6Mb+SUGHAXM0KNbcBPguytWyvFxcHgMLe2D2XSpw==", + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.13.0.tgz", + "integrity": "sha512-PRA911Blj99jR5RMeTunVbNXMF6Lp4vZXnk5GQjcnUWUTsrXtekg/pnmFFI2u/I36Y/2bITGS30GZCXei6uNkA==", "dev": true, "requires": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" } }, "ansi-styles": { @@ -8882,7 +8891,7 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, "through": { @@ -8967,7 +8976,7 @@ "tsmlb": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/tsmlb/-/tsmlb-1.0.0.tgz", - "integrity": "sha1-wnDimdh9pfAPmjJJvEJLUaQhTWc=", + "integrity": "sha512-FjFkSk95wsBE5x+HnfwTUMC88LTv+QwSiAH11OwwhIu01UjIZChnAxTUCSUiZ0B9Dks66CsK0CM8loqGhXDjgw==", "dev": true }, "type-check": { @@ -9038,6 +9047,12 @@ "which-boxed-primitive": "^1.0.2" } }, + "underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ==", + "dev": true + }, "unicode-canonical-property-names-ecmascript": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", @@ -9182,9 +9197,9 @@ "dev": true }, "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz", + "integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==", "dev": true }, "validate-npm-package-license": { @@ -9325,17 +9340,17 @@ } }, "videojs-standard": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/videojs-standard/-/videojs-standard-9.0.1.tgz", - "integrity": "sha512-klpkgUD8qgMNTP3nELWdJ41iNSBVSyN0lllambdcsZkEBnCIUoucv7DtL/um6rpSgaw2j+nQYQusTa0OoY/IEQ==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/videojs-standard/-/videojs-standard-9.1.0.tgz", + "integrity": "sha512-PcGGksY8Z2QkdiHVvWrOFDqDvt2ppO3ZWRaFrpDTICkl9sy2qRXOxYd9iQ2eqFQxhpWy2z408nyzMn4xghMeCw==", "dev": true, "requires": { "commander": "^7.2.0", "eslint": "^7.28.0", - "eslint-config-videojs": "^6.0.0", + "eslint-config-videojs": "^6.1.0", "eslint-plugin-jsdoc": "^35.3.0", "eslint-plugin-json-light": "^1.0.3", - "eslint-plugin-markdown": "^2.2.0", + "eslint-plugin-markdown": "^3.0.0", "find-root": "^1.1.0", "tsmlb": "^1.0.0" }, diff --git a/package.json b/package.json index 2db6c25a3..31240b086 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "videojs-generate-karma-config": "^8.0.1", "videojs-generate-rollup-config": "^7.0.0", "videojs-generator-verify": "~3.0.1", - "videojs-standard": "^9.0.0", + "videojs-standard": "^9.1.0", "water-plant-uml": "^2.0.2" }, "generator-videojs-plugin": { diff --git a/src/playlist-controller.js b/src/playlist-controller.js index 2118dc482..f24c5ab70 100644 --- a/src/playlist-controller.js +++ b/src/playlist-controller.js @@ -185,6 +185,7 @@ export class PlaylistController extends videojs.EventTarget { this.withCredentials = withCredentials; this.tech_ = tech; this.vhs_ = tech.vhs; + this.player_ = options.player_; this.sourceType_ = sourceType; this.useCueTags_ = useCueTags; this.playlistExclusionDuration = playlistExclusionDuration; @@ -762,7 +763,8 @@ export class PlaylistController extends videojs.EventTarget { playlistLoaderEvents.forEach((eventName) => { this.mainPlaylistLoader_.on(eventName, (metadata) => { - this.trigger({...metadata}); + // trigger directly on the player to ensure early events are fired. + this.player_.trigger({...metadata}); }); }); } @@ -1005,15 +1007,15 @@ export class PlaylistController extends videojs.EventTarget { segmentLoaderEvents.forEach((eventName) => { this.mainSegmentLoader_.on(eventName, (metadata) => { - this.trigger({...metadata}); + this.player_.trigger({...metadata}); }); this.audioSegmentLoader_.on(eventName, (metadata) => { - this.trigger({...metadata}); + this.player_.trigger({...metadata}); }); this.subtitleSegmentLoader_.on(eventName, (metadata) => { - this.trigger({...metadata}); + this.player_.trigger({...metadata}); }); }); diff --git a/src/playlist-loader.js b/src/playlist-loader.js index e232e7215..febdeb0f2 100644 --- a/src/playlist-loader.js +++ b/src/playlist-loader.js @@ -937,7 +937,7 @@ export default class PlaylistLoader extends EventTarget { }); // we haven't loaded any variant playlists here so we default to false for isLive. - metadata.parsedPlaylist = playlistMetadataPayload(manifest.playlists, metadata.type, false); + metadata.parsedPlaylist = playlistMetadataPayload(manifest.playlists, metadata.playlistInfo.type, false); this.trigger({ type: 'playlistparsecomplete', metadata }); this.setupInitialPlaylist(manifest); diff --git a/src/videojs-http-streaming.js b/src/videojs-http-streaming.js index 6ec83940d..2ad41e134 100644 --- a/src/videojs-http-streaming.js +++ b/src/videojs-http-streaming.js @@ -795,6 +795,8 @@ class VhsHandler extends Component { this.options_.seekTo = (time) => { this.tech_.setCurrentTime(time); }; + // pass player to allow for player level eventing on construction. + this.options_.player_ = this.player_; this.playlistController_ = new PlaylistController(this.options_); @@ -1327,33 +1329,6 @@ class VhsHandler extends Component { attachStreamingEventListeners_() { const playlistControllerEvents = [ - 'manifestrequeststart', - 'manifestrequestcomplete', - 'manifestparsestart', - 'manifestparsecomplete', - 'playlistrequeststart', - 'playlistrequestcomplete', - 'playlistparsestart', - 'playlistparsecomplete', - 'segmentselected', - 'segmentloadstart', - 'segmentloaded', - 'segmentkeyloadstart', - 'segmentkeyloadcomplete', - 'segmentdecryptionstart', - 'segmentdecryptioncomplete', - 'segmenttransmuxingstart', - 'segmenttransmuxingcomplete', - 'segmenttransmuxingtrackinfoavailable', - 'segmenttransmuxingtiminginfoavailable', - 'segmentappendstart', - 'appendsdone', - 'renditiondisabled', - 'renditionenabled', - 'renditionselected', - 'bandwidthupdated', - 'timelinechange', - 'codecschange', 'seekablerangeschanged', 'bufferedrangeschanged', 'contentsteeringloadstart', diff --git a/test/playback.test.js b/test/playback.test.js index ef2835549..248c2a59f 100644 --- a/test/playback.test.js +++ b/test/playback.test.js @@ -526,3 +526,160 @@ QUnit.test('hls manifest object', function(assert) { done(); }); }); + +QUnit.test('Advanced Bip Bop playlist events', function(assert) { + const done = assert.async(); + + this.player.defaultPlaybackRate(1); + + assert.expect(9); + const player = this.player; + let playlistrequeststartCallCount = 0; + let playlistparsestartCallCount = 0; + let playlistrequestcompleteCallCount = 0; + let playlistparsecompleteCallCount = 0; + + // playlist request start + player.on('playlistrequeststart', (event) => { + const expectedMetadata = { + playlistInfo: { + type: 'multivariant', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8' + } + }; + + if (playlistrequeststartCallCount === 1) { + expectedMetadata.playlistInfo.type = 'media'; + expectedMetadata.playlistInfo.uri = 'gear1/prog_index.m3u8'; + } + assert.deepEqual(event.metadata, expectedMetadata, 'playlistrequeststart got expected metadata'); + playlistrequeststartCallCount++; + }); + + // playlist request complete + player.on('playlistrequestcomplete', (event) => { + const expectedMetadata = { + playlistInfo: { + type: 'multivariant', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8' + } + }; + + if (playlistrequestcompleteCallCount === 1) { + expectedMetadata.playlistInfo.type = 'media'; + expectedMetadata.playlistInfo.uri = 'gear1/prog_index.m3u8'; + } + assert.deepEqual(event.metadata, expectedMetadata, 'playlistrequestcomplete got expected metadata'); + playlistrequestcompleteCallCount++; + }); + + // playlist parse start + player.on('playlistparsestart', (event) => { + const expectedMetadata = { + playlistInfo: { + type: 'multivariant', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8' + } + }; + + if (playlistparsestartCallCount === 1) { + expectedMetadata.playlistInfo.type = 'media'; + expectedMetadata.playlistInfo.uri = 'gear1/prog_index.m3u8'; + } + assert.deepEqual(event.metadata, expectedMetadata, 'playlistparsestart got expected metadata'); + playlistparsestartCallCount++; + }); + + // playlist parse complete + player.on('playlistparsecomplete', (event) => { + const expectedMetadata = { + playlistInfo: { + type: 'multivariant', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8' + }, + parsedPlaylist: { + isLive: false, + renditions: [ + { + bandwidth: 263851, + codecs: 'mp4a.40.2, avc1.4d400d', + id: undefined, + resolution: { + height: 234, + width: 416 + } + }, + { + bandwidth: 577610, + codecs: 'mp4a.40.2, avc1.4d401e', + id: undefined, + resolution: { + height: 360, + width: 640 + } + }, + { + bandwidth: 915905, + codecs: 'mp4a.40.2, avc1.4d401f', + id: undefined, + resolution: { + height: 540, + width: 960 + } + }, + { + bandwidth: 1030138, + codecs: 'mp4a.40.2, avc1.4d401f', + id: undefined, + resolution: { + height: 720, + width: 1280 + } + }, + { + bandwidth: 1924009, + codecs: 'mp4a.40.2, avc1.4d401f', + id: undefined, + resolution: { + height: 1080, + width: 1920 + } + }, + { + bandwidth: 41457, + codecs: 'mp4a.40.2', + id: undefined, + resolution: undefined + } + ], + type: 'multivariant' + } + }; + + if (playlistparsecompleteCallCount === 1) { + expectedMetadata.playlistInfo.type = 'media'; + expectedMetadata.playlistInfo.uri = 'gear1/prog_index.m3u8'; + expectedMetadata.parsedPlaylist.type = 'media'; + expectedMetadata.renditions[0].id = '0-gear1/prog_index.m3u8'; + expectedMetadata.renditions[1].id = '1-gear2/prog_index.m3u8'; + expectedMetadata.renditions[2].id = '2-gear3/prog_index.m3u8'; + expectedMetadata.renditions[3].id = '3-gear4/prog_index.m3u8'; + expectedMetadata.renditions[3].id = '4-gear5/prog_index.m3u8'; + expectedMetadata.renditions[3].id = '5-gear0/prog_index.m3u8'; + } + assert.deepEqual(event.metadata, expectedMetadata, 'playlistparsecomplete got expected metadata'); + playlistparsecompleteCallCount++; + }); + + playFor(player, 2, function() { + assert.ok(true, 'played for at least two seconds'); + assert.equal(player.error(), null, 'has no player errors'); + + done(); + }); + + player.src({ + src: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8', + type: 'application/x-mpegURL' + }); +}); diff --git a/test/playlist-controller.test.js b/test/playlist-controller.test.js index 85a45edb1..ebded8451 100644 --- a/test/playlist-controller.test.js +++ b/test/playlist-controller.test.js @@ -271,7 +271,8 @@ QUnit.test('getAudioTrackPlaylists_ invalid audio groups', function(assert) { QUnit.test('throws error when given an empty URL', function(assert) { const options = { src: 'test', - tech: this.player.tech_ + tech: this.player.tech_, + player_: this.player }; const controller = new PlaylistController(options); @@ -323,7 +324,8 @@ QUnit.test('obeys auto preload option', function(assert) { QUnit.test('passes options to PlaylistLoader', function(assert) { const options = { src: 'test', - tech: this.player.tech_ + tech: this.player.tech_, + player_: this.player }; let controller = new PlaylistController(options); @@ -344,7 +346,8 @@ QUnit.test('addMetadataToTextTrack adds expected metadata to the metadataTrack', const options = { src: 'test.mpd', tech: this.player.tech_, - sourceType: 'dash' + sourceType: 'dash', + player_: this.player }; // Test messageData property manifest @@ -461,7 +464,8 @@ QUnit.test('addDateRangesToTextTrack adds expected metadata to the metadataTrack const options = { src: 'manifest/daterange.m3u8', tech: this.player.tech_, - sourceType: 'hls' + sourceType: 'hls', + player_: this.player }; const controller = new PlaylistController(options); const dateRanges = [{ @@ -522,7 +526,8 @@ QUnit.test('creates appropriate PlaylistLoader for sourceType', function(assert) const options = { src: 'test', tech: this.player.tech_, - sourceType: 'hls' + sourceType: 'hls', + player_: this.player }; let pc = new PlaylistController(options); @@ -555,7 +560,8 @@ QUnit.test('creates appropriate PlaylistLoader for sourceType', function(assert) QUnit.test('passes options to SegmentLoader', function(assert) { const options = { src: 'test', - tech: this.player.tech_ + tech: this.player.tech_, + player_: this.player }; let controller = new PlaylistController(options); @@ -755,7 +761,7 @@ QUnit.test('resets everything for a fast quality change', function(assert) { QUnit.test('loadVttJs should be passed to the vttSegmentLoader and resolved on vttjsloaded', function(assert) { const stub = sinon.stub(this.player.tech_, 'addWebVttScript_').callsFake(() => this.player.tech_.trigger('vttjsloaded')); - const controller = new PlaylistController({ src: 'test', tech: this.player.tech_}); + const controller = new PlaylistController({ src: 'test', tech: this.player.tech_, player_: this.player }); controller.subtitleSegmentLoader_.loadVttJs().then(() => { assert.equal(stub.callCount, 1, 'tech addWebVttScript called once'); @@ -764,7 +770,7 @@ QUnit.test('loadVttJs should be passed to the vttSegmentLoader and resolved on v QUnit.test('loadVttJs should be passed to the vttSegmentLoader and rejected on vttjserror', function(assert) { const stub = sinon.stub(this.player.tech_, 'addWebVttScript_').callsFake(() => this.player.tech_.trigger('vttjserror')); - const controller = new PlaylistController({ src: 'test', tech: this.player.tech_}); + const controller = new PlaylistController({ src: 'test', tech: this.player.tech_, player_: this.player }); controller.subtitleSegmentLoader_.loadVttJs().catch(() => { assert.equal(stub.callCount, 1, 'tech addWebVttScript called once'); @@ -4966,7 +4972,8 @@ QUnit.test('excludeNonUsablePlaylistsByKeyId_ excludes non usable DASH playlists const options = { src: 'test', tech: this.player.tech_, - sourceType: 'dash' + sourceType: 'dash', + player_: this.player }; const pc = new PlaylistController(options); @@ -5011,7 +5018,8 @@ QUnit.test('excludeNonUsablePlaylistsByKeyId_ re includes non usable DASH playli const options = { src: 'test', tech: this.player.tech_, - sourceType: 'dash' + sourceType: 'dash', + player_: this.player }; const pc = new PlaylistController(options); @@ -5065,7 +5073,8 @@ QUnit.test('excludeNonUsablePlaylistsByKeyId_ re-includes SD playlists when all const options = { src: 'test', tech: this.player.tech_, - sourceType: 'dash' + sourceType: 'dash', + player_: this.player }; const pc = new PlaylistController(options); const origWarn = videojs.log.warn; @@ -6663,7 +6672,8 @@ QUnit.module('PlaylistController contentSteering', { this.controllerOptions = { src: 'test', tech: this.player.tech_, - sourceType: 'dash' + sourceType: 'dash', + player_: this.player }; this.csMainPlaylist = { @@ -6713,7 +6723,8 @@ QUnit.test('initContentSteeringController_ for HLS', function(assert) { const options = { src: 'test', tech: this.player.tech_, - sourceType: 'hls' + sourceType: 'hls', + player_: this.player }; const pc = new PlaylistController(options); @@ -7225,7 +7236,8 @@ QUnit.test('Pathway cloning - add a new pathway when the clone has not existed', const options = { src: 'test', tech: this.player.tech_, - sourceType: 'hls' + sourceType: 'hls', + player_: this.player }; const pc = new PlaylistController(options); @@ -7286,7 +7298,8 @@ QUnit.test('Pathway cloning - update the pathway when the BASE-ID does not match const options = { src: 'test', tech: this.player.tech_, - sourceType: 'hls' + sourceType: 'hls', + player_: this.player }; const pc = new PlaylistController(options); @@ -7360,7 +7373,8 @@ QUnit.test('Pathway cloning - update the pathway when there is a new param', fun const options = { src: 'test', tech: this.player.tech_, - sourceType: 'hls' + sourceType: 'hls', + player_: this.player }; const pc = new PlaylistController(options); @@ -7436,7 +7450,8 @@ QUnit.test('Pathway cloning - update the pathway when a param is missing', funct const options = { src: 'test', tech: this.player.tech_, - sourceType: 'hls' + sourceType: 'hls', + player_: this.player }; const pc = new PlaylistController(options); @@ -7509,7 +7524,8 @@ QUnit.test('Pathway cloning - delete the pathway when it is no longer in the ste const options = { src: 'test', tech: this.player.tech_, - sourceType: 'hls' + sourceType: 'hls', + player_: this.player }; const pc = new PlaylistController(options); @@ -7570,7 +7586,8 @@ QUnit.test('Pathway cloning - do nothing when next and past clones are the same' const options = { src: 'test', tech: this.player.tech_, - sourceType: 'hls' + sourceType: 'hls', + player_: this.player }; const pc = new PlaylistController(options); From 7b5f063ba927eddd852894766f14b2fad1466f2f Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Fri, 10 May 2024 11:25:08 -0700 Subject: [PATCH 09/14] chore: add playlist and segment event tests --- src/media-segment-request.js | 4 +- src/segment-loader.js | 50 ++++++++++----------- src/segment-transmuxer.js | 4 +- test/playback.test.js | 86 ++++++++++++++++++++++++++++++++---- 4 files changed, 106 insertions(+), 38 deletions(-) diff --git a/src/media-segment-request.js b/src/media-segment-request.js index 82ed08988..9f6c468f0 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -10,7 +10,7 @@ import { } from '@videojs/vhs-utils/es/containers'; import {merge} from './util/vjs-compat'; import { getStreamingNetworkErrorMetadata } from './error-codes.js'; -import { getSegmentInfoFromSimpleSegment } from './segment-loader.js'; +import { segmentInfoPayload } from './segment-loader.js'; export const REQUEST_ERRORS = { FAILURE: 2, @@ -633,7 +633,7 @@ const decrypt = function({id, key, encryptedBytes, decryptionWorker, segment, do decryptionWorker.onerror = () => { const message = 'An error occurred in the decryption worker'; - const segmentInfo = getSegmentInfoFromSimpleSegment(segment); + const segmentInfo = segmentInfoPayload({segment}); const decryptError = { message, metadata: { diff --git a/src/segment-loader.js b/src/segment-loader.js index 4fbc61546..6e48da2ce 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -511,36 +511,23 @@ export const getTroublesomeSegmentDurationMessage = (segmentInfo, sourceType) => }; /** - * Utility function to reduce a segmentInfo object to a simple event payload. * - * @param {SegmentInfo} segmentInfo the full SegmentInfo object to be reduced. - * @return the reduced payload. + * @param {Object} options type of segment loader and segment either segmentInfo or simple segment + * @return a segmentInfo payload for events or errors. */ -export const getSegmentInfoFromSimpleSegment = (segment) => { +export const segmentInfoPayload = ({type, segment}) => { if (!segment) { return; } - const { type, resolvedUri, start, duration, isEncrypted, isMediaInitialization } = segment; + const isEncrypted = Boolean(segment.key || segment.map && segment.map.ke); + const isMediaInitialization = Boolean(segment.map && !segment.map.bytes); + const start = segment.startOfSegment === undefined ? segment.start : segment.startOfSegment; return { - type, - uri: resolvedUri, + type: type || segment.type, + uri: segment.uri || segment.resolvedUri, start, - duration, - isEncrypted, - isMediaInitialization - }; -}; - -const segmentInfoPayload = (type, segmentInfo) => { - const isEncrypted = segmentInfo.segment.key || segmentInfo.segment.map && segmentInfo.segment.map.key; - const isMediaInitialization = segmentInfo.segment.map && !segmentInfo.segment.map.bytes; - - return { - type, - uri: segmentInfo.uri, - start: segmentInfo.startOfSegment, - duration: segmentInfo.duration, + duration: segment.duration, isEncrypted, isMediaInitialization }; @@ -1426,7 +1413,7 @@ bufferedEnd: ${lastBufferedEnd(this.buffered_())} } const metadata = { - segmentInfo: segmentInfoPayload(this.loaderType_, segmentInfo) + segmentInfo: segmentInfoPayload({type: this.loaderType_, segment: segmentInfo}) }; this.trigger({ type: 'segmentselected', metadata }); @@ -2517,7 +2504,7 @@ Fetch At Buffer: ${this.fetchAtBuffer_} }); } const metadata = { - segmentInfo: segmentInfoPayload(this.loaderType_, segmentInfo) + segmentInfo: segmentInfoPayload({type: this.loaderType_, segment: segmentInfo}) }; this.trigger({ type: 'segmentappendstart', metadata }); @@ -2690,8 +2677,19 @@ ${segmentInfoString(segmentInfo)}`); this.logger_(`${segmentInfoString(segmentInfo)} logged from transmuxer stream ${stream} as a ${level}: ${message}`); }, triggerSegmentEventFn: ({ type, segment, keyInfo, trackInfo, timingInfo }) => { - const segInfo = getSegmentInfoFromSimpleSegment(segment); - const metadata = { segmentInfo: segInfo, keyInfo, trackInfo, timingInfo }; + const segInfo = segmentInfoPayload({segment}); + const metadata = { segmentInfo: segInfo }; + // add other properties if necessary. + + if (keyInfo) { + metadata.keyInfo = keyInfo; + } + if (trackInfo) { + metadata.trackInfo = trackInfo; + } + if (timingInfo) { + metadata.timingInfo = timingInfo; + } this.trigger({ type, metadata }); } diff --git a/src/segment-transmuxer.js b/src/segment-transmuxer.js index 0fbec7e21..9db17e16f 100644 --- a/src/segment-transmuxer.js +++ b/src/segment-transmuxer.js @@ -1,6 +1,6 @@ import TransmuxWorker from 'worker!./transmuxer-worker.js'; import videojs from 'video.js'; -import { getSegmentInfoFromSimpleSegment } from './segment-loader'; +import { segmentInfoPayload } from './segment-loader'; export const handleData_ = (event, transmuxedData, callback) => { const { @@ -162,7 +162,7 @@ export const processTransmux = (options) => { message: 'Received an error message from the transmuxer worker', metadata: { errorType: videojs.Error.StreamingFailedToTransmuxSegment, - segmentInfo: getSegmentInfoFromSimpleSegment(segment) + segmentInfo: segmentInfoPayload({segment}) } }; diff --git a/test/playback.test.js b/test/playback.test.js index 248c2a59f..476bed257 100644 --- a/test/playback.test.js +++ b/test/playback.test.js @@ -532,7 +532,7 @@ QUnit.test('Advanced Bip Bop playlist events', function(assert) { this.player.defaultPlaybackRate(1); - assert.expect(9); + assert.expect(10); const player = this.player; let playlistrequeststartCallCount = 0; let playlistparsestartCallCount = 0; @@ -660,18 +660,88 @@ QUnit.test('Advanced Bip Bop playlist events', function(assert) { expectedMetadata.playlistInfo.type = 'media'; expectedMetadata.playlistInfo.uri = 'gear1/prog_index.m3u8'; expectedMetadata.parsedPlaylist.type = 'media'; - expectedMetadata.renditions[0].id = '0-gear1/prog_index.m3u8'; - expectedMetadata.renditions[1].id = '1-gear2/prog_index.m3u8'; - expectedMetadata.renditions[2].id = '2-gear3/prog_index.m3u8'; - expectedMetadata.renditions[3].id = '3-gear4/prog_index.m3u8'; - expectedMetadata.renditions[3].id = '4-gear5/prog_index.m3u8'; - expectedMetadata.renditions[3].id = '5-gear0/prog_index.m3u8'; + expectedMetadata.parsedPlaylist.renditions[0].id = '0-gear1/prog_index.m3u8'; + expectedMetadata.parsedPlaylist.renditions[1].id = '1-gear2/prog_index.m3u8'; + expectedMetadata.parsedPlaylist.renditions[2].id = '2-gear3/prog_index.m3u8'; + expectedMetadata.parsedPlaylist.renditions[3].id = '3-gear4/prog_index.m3u8'; + expectedMetadata.parsedPlaylist.renditions[4].id = '4-gear5/prog_index.m3u8'; + expectedMetadata.parsedPlaylist.renditions[5].id = '5-gear0/prog_index.m3u8'; } assert.deepEqual(event.metadata, expectedMetadata, 'playlistparsecomplete got expected metadata'); playlistparsecompleteCallCount++; }); - playFor(player, 2, function() { + playFor(player, 0.1, function() { + assert.ok(true, 'played for at least two seconds'); + assert.equal(player.error(), null, 'has no player errors'); + + done(); + }); + + player.src({ + src: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/bipbop_16x9_variant.m3u8', + type: 'application/x-mpegURL' + }); +}); + +QUnit.test('Advanced Bip Bop segment events', function(assert) { + const done = assert.async(); + + this.player.defaultPlaybackRate(1); + + // assert.expect(9); + const player = this.player; + + // segment selected + player.one('segmentselected', (event) => { + // console.log(event); + const expectedMetadata = { + segmentInfo: { + duration: 9.9766, + isEncrypted: false, + isMediaInitialization: false, + start: 0, + type: 'main', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'segmentselected got expected metadata'); + }); + + // segment load start + player.one('segmentloadstart', (event) => { + const expectedMetadata = { + segmentInfo: { + duration: 9.9766, + isEncrypted: false, + isMediaInitialization: false, + start: 0, + type: 'main', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'segmentloadstart got expected metadata'); + }); + + // segment loaded + player.one('segmentloaded', (event) => { + const expectedMetadata = { + segmentInfo: { + duration: 9.9766, + isEncrypted: false, + isMediaInitialization: false, + start: 0, + type: 'main', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'segmentloaded got expected metadata'); + }); + + playFor(player, 0.1, function() { assert.ok(true, 'played for at least two seconds'); assert.equal(player.error(), null, 'has no player errors'); From dd457d9ecdcb5fef73ab2296b65a7b3d253d1e0e Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Fri, 10 May 2024 12:04:10 -0700 Subject: [PATCH 10/14] chore: manifest event tests --- test/playback.test.js | 160 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 158 insertions(+), 2 deletions(-) diff --git a/test/playback.test.js b/test/playback.test.js index 476bed257..6dd163469 100644 --- a/test/playback.test.js +++ b/test/playback.test.js @@ -684,17 +684,173 @@ QUnit.test('Advanced Bip Bop playlist events', function(assert) { }); }); +QUnit.test('Big Buck Bunny manifest events', function(assert) { + const done = assert.async(); + + this.player.defaultPlaybackRate(1); + + assert.expect(6); + const player = this.player; + + player.one('manifestrequeststart', (event) => { + const expectedMetadata = { + manifestInfo: { + uri: 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'manifestrequeststart got expected metadata'); + }); + + player.one('manifestrequestcomplete', (event) => { + const expectedMetadata = { + manifestInfo: { + uri: 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'manifestrequestcomplete got expected metadata'); + }); + + player.one('manifestparsestart', (event) => { + const expectedMetadata = { + manifestInfo: { + uri: 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'manifestparsestart got expected metadata'); + }); + + player.one('manifestparsecomplete', (event) => { + const expectedMetadata = { + manifestInfo: { + uri: 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd' + }, + parsedManifest: { + duration: 634.566, + isLive: false, + renditions: [ + { + bandwidth: 3134488, + codecs: 'avc1.64001f', + id: '0-placeholder-uri-0', + resolution: { + height: 576, + width: 1024 + } + }, + { + bandwidth: 4952892, + codecs: 'avc1.64001f', + id: '1-placeholder-uri-1', + resolution: { + height: 720, + width: 1280 + } + }, + { + bandwidth: 9914554, + codecs: 'avc1.640028', + id: '2-placeholder-uri-2', + resolution: { + height: 1080, + width: 1920 + } + }, + { + bandwidth: 254320, + codecs: 'avc1.64000d', + id: '3-placeholder-uri-3', + resolution: { + height: 180, + width: 320 + } + }, + { + bandwidth: 507246, + codecs: 'avc1.64000d', + id: '4-placeholder-uri-4', + resolution: { + height: 180, + width: 320 + } + }, + { + bandwidth: 759798, + codecs: 'avc1.640015', + id: '5-placeholder-uri-5', + resolution: { + height: 270, + width: 480 + } + }, + { + bandwidth: 1254758, + codecs: 'avc1.64001e', + id: '6-placeholder-uri-6', + resolution: { + height: 360, + width: 640 + } + }, + { + bandwidth: 1013310, + codecs: 'avc1.64001e', + id: '7-placeholder-uri-7', + resolution: { + height: 360, + width: 640 + } + }, + { + bandwidth: 1883700, + codecs: 'avc1.64001e', + id: '8-placeholder-uri-8', + resolution: { + height: 432, + width: 768 + } + }, + { + bandwidth: 14931538, + codecs: 'avc1.640033', + id: '9-placeholder-uri-9', + resolution: { + height: 2160, + width: 3840 + } + } + ] + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'manifestparsestart got expected metadata'); + }); + + playFor(player, 0.1, function() { + assert.ok(true, 'played for at least two seconds'); + assert.equal(player.error(), null, 'has no player errors'); + + done(); + }); + + player.src({ + src: 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd', + type: 'application/dash+xml' + }); +}); + QUnit.test('Advanced Bip Bop segment events', function(assert) { const done = assert.async(); this.player.defaultPlaybackRate(1); - // assert.expect(9); + assert.expect(5); const player = this.player; // segment selected player.one('segmentselected', (event) => { - // console.log(event); const expectedMetadata = { segmentInfo: { duration: 9.9766, From 4bd49e3cfc6fb9f8265beb9b398de0287d167bb3 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Sun, 12 May 2024 21:17:35 -0700 Subject: [PATCH 11/14] chore: segment tests --- src/media-segment-request.js | 8 +-- src/playlist-controller.js | 7 +-- src/segment-loader.js | 2 +- test/playback.test.js | 114 ++++++++++++++++++++++++++++++++--- 4 files changed, 113 insertions(+), 18 deletions(-) diff --git a/src/media-segment-request.js b/src/media-segment-request.js index 9f6c468f0..202e11be5 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -371,12 +371,12 @@ const transmuxAndNotify = ({ onVideoSegmentTimingInfo: (videoSegmentTimingInfo) => { const timingInfo = { pts: { - start: videoSegmentTimingInfo.start.pts, - end: videoSegmentTimingInfo.end.pts + start: videoSegmentTimingInfo.start.presentation, + end: videoSegmentTimingInfo.end.presentation }, dts: { - start: videoSegmentTimingInfo.start.dts, - end: videoSegmentTimingInfo.end.dts + start: videoSegmentTimingInfo.start.decode, + end: videoSegmentTimingInfo.end.decode } }; diff --git a/src/playlist-controller.js b/src/playlist-controller.js index f24c5ab70..1a32bb9b3 100644 --- a/src/playlist-controller.js +++ b/src/playlist-controller.js @@ -738,13 +738,10 @@ export class PlaylistController extends videojs.EventTarget { } }); - this.mainPlaylistLoader_.on('renditiondisabled', (metadata) => { - // eslint-disable-next-line - this.trigger({...metadata}); + this.mainPlaylistLoader_.on('renditiondisabled', () => { this.tech_.trigger({type: 'usage', name: 'vhs-rendition-disabled'}); }); - this.mainPlaylistLoader_.on('renditionenabled', (metadata) => { - this.trigger({...metadata}); + this.mainPlaylistLoader_.on('renditionenabled', () => { this.tech_.trigger({type: 'usage', name: 'vhs-rendition-enabled'}); }); diff --git a/src/segment-loader.js b/src/segment-loader.js index 6e48da2ce..870105a97 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -3178,7 +3178,7 @@ ${segmentInfoString(segmentInfo)}`); // appendsdone can cause an abort if (this.pendingSegment_) { const metadata = { - segmentInfo: segmentInfoPayload(this.loaderType_, this.pendingSegment_) + segmentInfo: segmentInfoPayload({type: this.loaderType_, segment: this.pendingSegment_}) }; this.trigger({ type: 'appendsdone', metadata}); diff --git a/test/playback.test.js b/test/playback.test.js index 6dd163469..901803f27 100644 --- a/test/playback.test.js +++ b/test/playback.test.js @@ -527,6 +527,7 @@ QUnit.test('hls manifest object', function(assert) { }); }); +// playlist event tests (hls specific) QUnit.test('Advanced Bip Bop playlist events', function(assert) { const done = assert.async(); @@ -539,7 +540,6 @@ QUnit.test('Advanced Bip Bop playlist events', function(assert) { let playlistrequestcompleteCallCount = 0; let playlistparsecompleteCallCount = 0; - // playlist request start player.on('playlistrequeststart', (event) => { const expectedMetadata = { playlistInfo: { @@ -556,7 +556,6 @@ QUnit.test('Advanced Bip Bop playlist events', function(assert) { playlistrequeststartCallCount++; }); - // playlist request complete player.on('playlistrequestcomplete', (event) => { const expectedMetadata = { playlistInfo: { @@ -573,7 +572,6 @@ QUnit.test('Advanced Bip Bop playlist events', function(assert) { playlistrequestcompleteCallCount++; }); - // playlist parse start player.on('playlistparsestart', (event) => { const expectedMetadata = { playlistInfo: { @@ -590,7 +588,6 @@ QUnit.test('Advanced Bip Bop playlist events', function(assert) { playlistparsestartCallCount++; }); - // playlist parse complete player.on('playlistparsecomplete', (event) => { const expectedMetadata = { playlistInfo: { @@ -684,6 +681,7 @@ QUnit.test('Advanced Bip Bop playlist events', function(assert) { }); }); +// manifest event tests (dash specific) QUnit.test('Big Buck Bunny manifest events', function(assert) { const done = assert.async(); @@ -841,15 +839,15 @@ QUnit.test('Big Buck Bunny manifest events', function(assert) { }); }); +// segment event tests QUnit.test('Advanced Bip Bop segment events', function(assert) { const done = assert.async(); this.player.defaultPlaybackRate(1); - assert.expect(5); + assert.expect(10); const player = this.player; - // segment selected player.one('segmentselected', (event) => { const expectedMetadata = { segmentInfo: { @@ -865,7 +863,6 @@ QUnit.test('Advanced Bip Bop segment events', function(assert) { assert.deepEqual(event.metadata, expectedMetadata, 'segmentselected got expected metadata'); }); - // segment load start player.one('segmentloadstart', (event) => { const expectedMetadata = { segmentInfo: { @@ -881,7 +878,6 @@ QUnit.test('Advanced Bip Bop segment events', function(assert) { assert.deepEqual(event.metadata, expectedMetadata, 'segmentloadstart got expected metadata'); }); - // segment loaded player.one('segmentloaded', (event) => { const expectedMetadata = { segmentInfo: { @@ -897,6 +893,108 @@ QUnit.test('Advanced Bip Bop segment events', function(assert) { assert.deepEqual(event.metadata, expectedMetadata, 'segmentloaded got expected metadata'); }); + // ts specific + player.one('segmenttransmuxingstart', (event) => { + const expectedMetadata = { + segmentInfo: { + duration: 9.9766, + isEncrypted: false, + isMediaInitialization: false, + start: 0, + type: 'main', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'segmenttransmuxingstart got expected metadata'); + }); + + player.one('segmenttransmuxingcomplete', (event) => { + const expectedMetadata = { + segmentInfo: { + duration: 9.9766, + isEncrypted: false, + isMediaInitialization: false, + start: 0, + type: 'main', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'segmenttransmuxingcomplete got expected metadata'); + }); + + // no trackInfoFn for some reason? + // player.one('segmenttransmuxingtrackinfoavailable', (event) => { + // const expectedMetadata = { + // segmentInfo: { + // duration: 9.9766, + // isEncrypted: false, + // isMediaInitialization: false, + // start: 0, + // type: 'main', + // uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' + // } + // }; + + // assert.deepEqual(event.metadata, expectedMetadata, 'segmenttransmuxingtrackinfoavailable got expected metadata'); + // }); + + player.one('segmenttransmuxingtiminginfoavailable', (event) => { + const expectedMetadata = { + segmentInfo: { + duration: 9.9766, + isEncrypted: false, + isMediaInitialization: false, + start: 0, + type: 'main', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' + }, + timingInfo: { + dts: { + end: 19.976644444444446, + start: 10 + }, + pts: { + end: 19.976644444444446, + start: 10 + } + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'segmenttransmuxingtiminginfoavailable got expected metadata'); + }); + + player.one('segmentappendstart', (event) => { + const expectedMetadata = { + segmentInfo: { + duration: 9.9766, + isEncrypted: false, + isMediaInitialization: false, + start: 0, + type: 'main', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'segmentappendstart got expected metadata'); + }); + + player.one('appendsdone', (event) => { + const expectedMetadata = { + segmentInfo: { + duration: 9.9766, + isEncrypted: false, + isMediaInitialization: false, + start: 0, + type: 'main', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'appendsdone got expected metadata'); + }); + playFor(player, 0.1, function() { assert.ok(true, 'played for at least two seconds'); assert.equal(player.error(), null, 'has no player errors'); From 1ca5171ba37868109cfa41feb1d42c6ab55edb82 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Sun, 12 May 2024 22:45:04 -0700 Subject: [PATCH 12/14] chore: more tests --- src/media-segment-request.js | 6 --- src/segment-loader.js | 12 +++++- test/playback.test.js | 82 +++++++++++++++++++++++++++++------- 3 files changed, 78 insertions(+), 22 deletions(-) diff --git a/src/media-segment-request.js b/src/media-segment-request.js index 202e11be5..934d7ea1a 100644 --- a/src/media-segment-request.js +++ b/src/media-segment-request.js @@ -338,12 +338,6 @@ const transmuxAndNotify = ({ trackInfo.isMuxed = true; } trackInfoFn(segment, trackInfo); - const info = { - hasAudio: trackInfo.hasAudio, - hasVideo: trackInfo.hasVideo - }; - - triggerSegmentEventFn({ type: 'segmenttransmuxingtrackinfoavailable', segment, trackInfo: info }); } }, onAudioTimingInfo: (audioTimingInfo) => { diff --git a/src/segment-loader.js b/src/segment-loader.js index 870105a97..010f1f04c 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -694,7 +694,7 @@ export default class SegmentLoader extends videojs.EventTarget { // see the shouldWaitForTimelineChange function. if (this.loaderType_ === 'audio') { this.timelineChangeController_.on('timelinechange', (metadata) => { - this.trigger({type: 'timelinechange', metadata }); + this.trigger({type: 'timelinechange', ...metadata }); if (this.hasEnoughInfoToLoad_()) { this.processLoadQueue_(); } @@ -1885,6 +1885,16 @@ Fetch At Buffer: ${this.fetchAtBuffer_} } handleTrackInfo_(simpleSegment, trackInfo) { + const { hasAudio, hasVideo } = trackInfo; + const metadata = { + segmentInfo: segmentInfoPayload({type: this.loaderType_, segment: simpleSegment}), + trackInfo: { + hasAudio, + hasVideo + } + }; + + this.trigger({type: 'segmenttransmuxingtrackinfoavailable', metadata}); this.earlyAbortWhenNeeded_(simpleSegment.stats); if (this.checkForAbort_(simpleSegment.requestId)) { diff --git a/test/playback.test.js b/test/playback.test.js index 901803f27..c91029f58 100644 --- a/test/playback.test.js +++ b/test/playback.test.js @@ -845,7 +845,7 @@ QUnit.test('Advanced Bip Bop segment events', function(assert) { this.player.defaultPlaybackRate(1); - assert.expect(10); + assert.expect(11); const player = this.player; player.one('segmentselected', (event) => { @@ -924,21 +924,24 @@ QUnit.test('Advanced Bip Bop segment events', function(assert) { assert.deepEqual(event.metadata, expectedMetadata, 'segmenttransmuxingcomplete got expected metadata'); }); - // no trackInfoFn for some reason? - // player.one('segmenttransmuxingtrackinfoavailable', (event) => { - // const expectedMetadata = { - // segmentInfo: { - // duration: 9.9766, - // isEncrypted: false, - // isMediaInitialization: false, - // start: 0, - // type: 'main', - // uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' - // } - // }; + player.one('segmenttransmuxingtrackinfoavailable', (event) => { + const expectedMetadata = { + segmentInfo: { + duration: 9.9766, + isEncrypted: false, + isMediaInitialization: false, + start: 0, + type: 'main', + uri: 'https://s3.amazonaws.com/_bc_dml/example-content/bipbop-advanced/gear1/main.ts' + }, + trackInfo: { + hasVideo: true, + hasAudio: true + } + }; - // assert.deepEqual(event.metadata, expectedMetadata, 'segmenttransmuxingtrackinfoavailable got expected metadata'); - // }); + assert.deepEqual(event.metadata, expectedMetadata, 'segmenttransmuxingtrackinfoavailable got expected metadata'); + }); player.one('segmenttransmuxingtiminginfoavailable', (event) => { const expectedMetadata = { @@ -1007,3 +1010,52 @@ QUnit.test('Advanced Bip Bop segment events', function(assert) { type: 'application/x-mpegURL' }); }); + +QUnit.test('Big Buck Bunny streaming events', function(assert) { + const done = assert.async(); + + this.player.defaultPlaybackRate(1); + + assert.expect(7); + const player = this.player; + + player.one('bandwidthupdated', (event) => { + assert.notOk(isNaN(event.metadata.bandwidthInfo.from), 'manifestrequeststart got expected metadata'); + assert.notOk(isNaN(event.metadata.bandwidthInfo.to), 'manifestrequeststart got expected metadata'); + }); + + player.one('timelinechange', (event) => { + const expectedMetadata = { + timelineChangeInfo: { + from: -1, + to: 0 + } + }; + + assert.deepEqual(event.metadata, expectedMetadata, 'timelinechange got expected metadata'); + }); + + player.one('seekablerangeschanged', (event) => { + assert.ok(event.metadata.seekableRanges, 'manifestparsestart got expected metadata'); + }); + + player.one('bufferedrangeschanged', (event) => { + assert.ok(event.metadata.bufferedRanges, 'manifestparsestart got expected metadata'); + }); + + player.one('playedrangeschanged', (event) => { + assert.ok(event.metadata.playedRanges, 'manifestparsestart got expected metadata'); + }); + + playFor(player, 0.1, function() { + assert.ok(true, 'played for at least two seconds'); + assert.equal(player.error(), null, 'has no player errors'); + + done(); + }); + + player.src({ + src: 'https://dash.akamaized.net/akamai/bbb_30fps/bbb_30fps.mpd', + type: 'application/dash+xml' + }); +}); From 206e9923256fa95e645b0ce3f5a8d8c51359f251 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Mon, 13 May 2024 09:36:00 -0700 Subject: [PATCH 13/14] fix: inner metadata --- src/segment-loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/segment-loader.js b/src/segment-loader.js index 010f1f04c..00be053b1 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -676,7 +676,7 @@ export default class SegmentLoader extends videojs.EventTarget { }); this.sourceUpdater_.on('codecschange', (metadata) => { - this.trigger({type: 'codecschange', metadata}); + this.trigger({type: 'codecschange', ...metadata}); }); // Only the main loader needs to listen for pending timeline changes, as the main // loader should wait for audio to be ready to change its timeline so that both main From a14d0e68e4e12e1926a6f9fe58bd855aa80654e5 Mon Sep 17 00:00:00 2001 From: Adam Waldron Date: Thu, 16 May 2024 11:33:23 -0700 Subject: [PATCH 14/14] fix: use resolvedUri first --- src/segment-loader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/segment-loader.js b/src/segment-loader.js index 00be053b1..fe1b201ca 100644 --- a/src/segment-loader.js +++ b/src/segment-loader.js @@ -525,7 +525,7 @@ export const segmentInfoPayload = ({type, segment}) => { return { type: type || segment.type, - uri: segment.uri || segment.resolvedUri, + uri: segment.resolvedUri || segment.uri, start, duration: segment.duration, isEncrypted,