Use a single file instead of segments for HLS
authorChocobozzz <me@florianbigard.com>
Thu, 7 Feb 2019 14:08:19 +0000 (15:08 +0100)
committerChocobozzz <chocobozzz@cpy.re>
Mon, 11 Feb 2019 08:13:02 +0000 (09:13 +0100)
16 files changed:
client/src/assets/player/p2p-media-loader/segment-validator.ts
package.json
scripts/generate-code-contributors.ts
server/helpers/ffmpeg-utils.ts
server/helpers/requests.ts
server/helpers/utils.ts
server/lib/activitypub/actor.ts
server/lib/hls.ts
server/lib/video-transcoding.ts
server/models/video/video-streaming-playlist.ts
server/tests/api/redundancy/redundancy.ts
server/tests/api/videos/video-hls.ts
shared/models/activitypub/activitypub-ordered-collection.ts
shared/utils/requests/requests.ts
shared/utils/videos/video-playlists.ts
yarn.lock

index 8f4922daa040f0ec6e1ec3bc01eba5c8229c3e97..72c32f9e058aa63de5aa79db4cd154a79f3285b8 100644 (file)
@@ -3,18 +3,25 @@ import { basename } from 'path'
 
 function segmentValidatorFactory (segmentsSha256Url: string) {
   const segmentsJSON = fetchSha256Segments(segmentsSha256Url)
+  const regex = /bytes=(\d+)-(\d+)/
 
   return async function segmentValidator (segment: Segment) {
-    const segmentName = basename(segment.url)
+    const filename = basename(segment.url)
+    const captured = regex.exec(segment.range)
 
-    const hashShouldBe = (await segmentsJSON)[segmentName]
+    const range = captured[1] + '-' + captured[2]
+
+    const hashShouldBe = (await segmentsJSON)[filename][range]
     if (hashShouldBe === undefined) {
-      throw new Error(`Unknown segment name ${segmentName} in segment validator`)
+      throw new Error(`Unknown segment name ${filename}/${range} in segment validator`)
     }
 
     const calculatedSha = bufferToEx(await sha256(segment.data))
     if (calculatedSha !== hashShouldBe) {
-      throw new Error(`Hashes does not correspond for segment ${segmentName} (expected: ${hashShouldBe} instead of ${calculatedSha})`)
+      throw new Error(
+        `Hashes does not correspond for segment ${filename}/${range}` +
+        `(expected: ${hashShouldBe} instead of ${calculatedSha})`
+      )
     }
   }
 }
index c8c9e64aed30db031e550d2db256b878d3e61383..0cf39c7ee4c5e4f51c2249317458398728ffd3f5 100644 (file)
     "fluent-ffmpeg": "^2.1.0",
     "fs-extra": "^7.0.0",
     "helmet": "^3.12.1",
-    "hlsdownloader": "https://github.com/Chocobozzz/hlsdownloader#build",
     "http-signature": "^1.2.0",
     "ip-anonymize": "^0.0.6",
     "ipaddr.js": "1.8.1",
index 9824bc2f5c2cdfe06d6fe1e34edebacd7347091a..96110307aa3b6c07f7a374577744c9a462400e3f 100755 (executable)
@@ -41,7 +41,7 @@ async function run () {
 }
 
 function get (url: string, headers: any = {}) {
-  return doRequest({
+  return doRequest<any>({
     uri: url,
     json: true,
     headers: Object.assign(headers, {
index 5ad8ed48e67e501942870f2770076371acac1fc3..133b1b03b0ac75e82afa0e648491b9d01d8c4cb4 100644 (file)
@@ -122,7 +122,9 @@ type TranscodeOptions = {
   resolution: VideoResolution
   isPortraitMode?: boolean
 
-  generateHlsPlaylist?: boolean
+  hlsPlaylist?: {
+    videoFilename: string
+  }
 }
 
 function transcode (options: TranscodeOptions) {
@@ -161,14 +163,16 @@ function transcode (options: TranscodeOptions) {
         command = command.withFPS(fps)
       }
 
-      if (options.generateHlsPlaylist) {
-        const segmentFilename = `${dirname(options.outputPath)}/${options.resolution}_%03d.ts`
+      if (options.hlsPlaylist) {
+        const videoPath = `${dirname(options.outputPath)}/${options.hlsPlaylist.videoFilename}`
 
         command = command.outputOption('-hls_time 4')
                          .outputOption('-hls_list_size 0')
                          .outputOption('-hls_playlist_type vod')
-                         .outputOption('-hls_segment_filename ' + segmentFilename)
+                         .outputOption('-hls_segment_filename ' + videoPath)
+                         .outputOption('-hls_segment_type fmp4')
                          .outputOption('-f hls')
+                         .outputOption('-hls_flags single_file')
       }
 
       command
index 3fc776f1a53299ceb2630313938d8c4757b24b33..5c6dc5e195638d997cc5acbb92dc72ac614fda99 100644 (file)
@@ -7,7 +7,7 @@ import { join } from 'path'
 
 function doRequest <T> (
   requestOptions: request.CoreOptions & request.UriOptions & { activityPub?: boolean }
-): Bluebird<{ response: request.RequestResponse, body: any }> {
+): Bluebird<{ response: request.RequestResponse, body: T }> {
   if (requestOptions.activityPub === true) {
     if (!Array.isArray(requestOptions.headers)) requestOptions.headers = {}
     requestOptions.headers['accept'] = ACTIVITY_PUB.ACCEPT_HEADER
index 3c3406e381724fc71ec5660aeb430470c553c68a..cb0e823c577717ffa1c3e186d8c6d98a3a50775a 100644 (file)
@@ -7,7 +7,6 @@ import { join } from 'path'
 import { Instance as ParseTorrent } from 'parse-torrent'
 import { remove } from 'fs-extra'
 import * as memoizee from 'memoizee'
-import { isArray } from './custom-validators/misc'
 
 function deleteFileAsync (path: string) {
   remove(path)
index 8215840da3eb0fdd525feb2943b6518cad30be47..a3f379b76938a29e6a587488611cbf468117346e 100644 (file)
@@ -355,10 +355,10 @@ async function fetchRemoteActor (actorUrl: string): Promise<{ statusCode?: numbe
 
   logger.info('Fetching remote actor %s.', actorUrl)
 
-  const requestResult = await doRequest(options)
+  const requestResult = await doRequest<ActivityPubActor>(options)
   normalizeActor(requestResult.body)
 
-  const actorJSON: ActivityPubActor = requestResult.body
+  const actorJSON = requestResult.body
   if (isActorObjectValid(actorJSON) === false) {
     logger.debug('Remote actor JSON is not valid.', { actorJSON })
     return { result: undefined, statusCode: requestResult.response.statusCode }
index 10db6c3c345ae0ce7bdabd7f9ccb3338a89fa5b3..3575981f4b05e39567271c86c04042fa4eb4ce2e 100644 (file)
@@ -1,13 +1,14 @@
 import { VideoModel } from '../models/video/video'
-import { basename, dirname, join } from 'path'
-import { HLS_PLAYLIST_DIRECTORY, CONFIG } from '../initializers'
-import { outputJSON, pathExists, readdir, readFile, remove, writeFile, move } from 'fs-extra'
+import { basename, join, dirname } from 'path'
+import { CONFIG, HLS_PLAYLIST_DIRECTORY } from '../initializers'
+import { close, ensureDir, move, open, outputJSON, pathExists, read, readFile, remove, writeFile } from 'fs-extra'
 import { getVideoFileSize } from '../helpers/ffmpeg-utils'
 import { sha256 } from '../helpers/core-utils'
 import { VideoStreamingPlaylistModel } from '../models/video/video-streaming-playlist'
-import HLSDownloader from 'hlsdownloader'
 import { logger } from '../helpers/logger'
-import { parse } from 'url'
+import { doRequest, doRequestAndSaveToFile } from '../helpers/requests'
+import { generateRandomString } from '../helpers/utils'
+import { flatten, uniq } from 'lodash'
 
 async function updateMasterHLSPlaylist (video: VideoModel) {
   const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
@@ -37,66 +38,119 @@ async function updateMasterHLSPlaylist (video: VideoModel) {
 }
 
 async function updateSha256Segments (video: VideoModel) {
-  const directory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
-  const files = await readdir(directory)
-  const json: { [filename: string]: string} = {}
+  const json: { [filename: string]: { [range: string]: string } } = {}
+
+  const playlistDirectory = join(HLS_PLAYLIST_DIRECTORY, video.uuid)
+
+  // For all the resolutions available for this video
+  for (const file of video.VideoFiles) {
+    const rangeHashes: { [range: string]: string } = {}
+
+    const videoPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution))
+    const playlistPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsPlaylistFilename(file.resolution))
 
-  for (const file of files) {
-    if (file.endsWith('.ts') === false) continue
+    // Maybe the playlist is not generated for this resolution yet
+    if (!await pathExists(playlistPath)) continue
 
-    const buffer = await readFile(join(directory, file))
-    const filename = basename(file)
+    const playlistContent = await readFile(playlistPath)
+    const ranges = getRangesFromPlaylist(playlistContent.toString())
 
-    json[filename] = sha256(buffer)
+    const fd = await open(videoPath, 'r')
+    for (const range of ranges) {
+      const buf = Buffer.alloc(range.length)
+      await read(fd, buf, 0, range.length, range.offset)
+
+      rangeHashes[`${range.offset}-${range.offset + range.length - 1}`] = sha256(buf)
+    }
+    await close(fd)
+
+    const videoFilename = VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, file.resolution)
+    json[videoFilename] = rangeHashes
   }
 
-  const outputPath = join(directory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
+  const outputPath = join(playlistDirectory, VideoStreamingPlaylistModel.getHlsSha256SegmentsFilename())
   await outputJSON(outputPath, json)
 }
 
-function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) {
-  let timer
+function getRangesFromPlaylist (playlistContent: string) {
+  const ranges: { offset: number, length: number }[] = []
+  const lines = playlistContent.split('\n')
+  const regex = /^#EXT-X-BYTERANGE:(\d+)@(\d+)$/
 
-  logger.info('Importing HLS playlist %s', playlistUrl)
+  for (const line of lines) {
+    const captured = regex.exec(line)
 
-  const params = {
-    playlistURL: playlistUrl,
-    destination: CONFIG.STORAGE.TMP_DIR
+    if (captured) {
+      ranges.push({ length: parseInt(captured[1], 10), offset: parseInt(captured[2], 10) })
+    }
   }
-  const downloader = new HLSDownloader(params)
-
-  const hlsDestinationDir = join(CONFIG.STORAGE.TMP_DIR, dirname(parse(playlistUrl).pathname))
 
-  return new Promise<string>(async (res, rej) => {
-    downloader.startDownload(err => {
-      clearTimeout(timer)
+  return ranges
+}
 
-      if (err) {
-        deleteTmpDirectory(hlsDestinationDir)
+function downloadPlaylistSegments (playlistUrl: string, destinationDir: string, timeout: number) {
+  let timer
 
-        return rej(err)
-      }
+  logger.info('Importing HLS playlist %s', playlistUrl)
 
-      move(hlsDestinationDir, destinationDir, { overwrite: true })
-        .then(() => res())
-        .catch(err => {
-          deleteTmpDirectory(hlsDestinationDir)
+  return new Promise<string>(async (res, rej) => {
+    const tmpDirectory = join(CONFIG.STORAGE.TMP_DIR, await generateRandomString(10))
 
-          return rej(err)
-        })
-    })
+    await ensureDir(tmpDirectory)
 
     timer = setTimeout(() => {
-      deleteTmpDirectory(hlsDestinationDir)
+      deleteTmpDirectory(tmpDirectory)
 
       return rej(new Error('HLS download timeout.'))
     }, timeout)
 
-    function deleteTmpDirectory (directory: string) {
-      remove(directory)
-        .catch(err => logger.error('Cannot delete path on HLS download error.', { err }))
+    try {
+      // Fetch master playlist
+      const subPlaylistUrls = await fetchUniqUrls(playlistUrl)
+
+      const subRequests = subPlaylistUrls.map(u => fetchUniqUrls(u))
+      const fileUrls = uniq(flatten(await Promise.all(subRequests)))
+
+      logger.debug('Will download %d HLS files.', fileUrls.length, { fileUrls })
+
+      for (const fileUrl of fileUrls) {
+        const destPath = join(tmpDirectory, basename(fileUrl))
+
+        await doRequestAndSaveToFile({ uri: fileUrl }, destPath)
+      }
+
+      clearTimeout(timer)
+
+      await move(tmpDirectory, destinationDir, { overwrite: true })
+
+      return res()
+    } catch (err) {
+      deleteTmpDirectory(tmpDirectory)
+
+      return rej(err)
     }
   })
+
+  function deleteTmpDirectory (directory: string) {
+    remove(directory)
+      .catch(err => logger.error('Cannot delete path on HLS download error.', { err }))
+  }
+
+  async function fetchUniqUrls (playlistUrl: string) {
+    const { body } = await doRequest<string>({ uri: playlistUrl })
+
+    if (!body) return []
+
+    const urls = body.split('\n')
+      .filter(line => line.endsWith('.m3u8') || line.endsWith('.mp4'))
+      .map(url => {
+        if (url.startsWith('http://') || url.startsWith('https://')) return url
+
+        return `${dirname(playlistUrl)}/${url}`
+      })
+
+    return uniq(urls)
+  }
 }
 
 // ---------------------------------------------------------------------------
index 608badfefc90d5040465317169d4337d4a23afb5..086b860a244ba92eb3f60d91a9f8d759a7a6b441 100644 (file)
@@ -100,7 +100,10 @@ async function generateHlsPlaylist (video: VideoModel, resolution: VideoResoluti
     outputPath,
     resolution,
     isPortraitMode,
-    generateHlsPlaylist: true
+
+    hlsPlaylist: {
+      videoFilename: VideoStreamingPlaylistModel.getHlsVideoName(video.uuid, resolution)
+    }
   }
 
   await transcode(transcodeOptions)
index bce537781aa76c371449bd41a763ca644d4b81c7..bf6f7b0c4c465ca0a07a2f7bbcd41e7edb7f9b37 100644 (file)
@@ -125,6 +125,10 @@ export class VideoStreamingPlaylistModel extends Model<VideoStreamingPlaylistMod
     return 'segments-sha256.json'
   }
 
+  static getHlsVideoName (uuid: string, resolution: number) {
+    return `${uuid}-${resolution}-fragmented.mp4`
+  }
+
   static getHlsMasterPlaylistStaticPath (videoUUID: string) {
     return join(STATIC_PATHS.PLAYLISTS.HLS, videoUUID, VideoStreamingPlaylistModel.getMasterHlsPlaylistFilename())
   }
index 5b99309fb1ada934e59f2657b80e2e913bf71a1d..778611fffbab81890a68338a2677383b624af3ac 100644 (file)
@@ -17,7 +17,7 @@ import {
   viewVideo,
   wait,
   waitUntilLog,
-  checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer
+  checkVideoFilesWereRemoved, removeVideo, getVideoWithToken, reRunServer, checkSegmentHash
 } from '../../../../shared/utils'
 import { waitJobs } from '../../../../shared/utils/server/jobs'
 
@@ -178,20 +178,24 @@ async function check1PlaylistRedundancies (videoUUID?: string) {
     expect(redundancy.baseUrl).to.equal(servers[0].url + '/static/redundancy/hls/' + videoUUID)
   }
 
-  await makeGetRequest({
-    url: servers[0].url,
-    statusCodeExpected: 200,
-    path: `/static/redundancy/hls/${videoUUID}/360_000.ts`,
-    contentType: null
-  })
+  const baseUrlPlaylist = servers[1].url + '/static/playlists/hls'
+  const baseUrlSegment = servers[0].url + '/static/redundancy/hls'
+
+  const res = await getVideo(servers[0].url, videoUUID)
+  const hlsPlaylist = (res.body as VideoDetails).streamingPlaylists[0]
+
+  for (const resolution of [ 240, 360, 480, 720 ]) {
+    await checkSegmentHash(baseUrlPlaylist, baseUrlSegment, videoUUID, resolution, hlsPlaylist)
+  }
 
   for (const directory of [ 'test1/redundancy/hls', 'test2/playlists/hls' ]) {
     const files = await readdir(join(root(), directory, videoUUID))
     expect(files).to.have.length.at.least(4)
 
     for (const resolution of [ 240, 360, 480, 720 ]) {
-      expect(files.find(f => f === `${resolution}_000.ts`)).to.not.be.undefined
-      expect(files.find(f => f === `${resolution}_001.ts`)).to.not.be.undefined
+      const filename = `${videoUUID}-${resolution}-fragmented.mp4`
+
+      expect(files.find(f => f === filename)).to.not.be.undefined
     }
   }
 }
index 71d863b127630fcb38d15472fe1d5f5715f0ef08..a1214bad165b5d46d9152625bf9b90fa310cc90c 100644 (file)
@@ -4,13 +4,12 @@ import * as chai from 'chai'
 import 'mocha'
 import {
   checkDirectoryIsEmpty,
+  checkSegmentHash,
   checkTmpIsEmpty,
   doubleFollow,
   flushAndRunMultipleServers,
   flushTests,
   getPlaylist,
-  getSegment,
-  getSegmentSha256,
   getVideo,
   killallServers,
   removeVideo,
@@ -22,7 +21,6 @@ import {
 } from '../../../../shared/utils'
 import { VideoDetails } from '../../../../shared/models/videos'
 import { VideoStreamingPlaylistType } from '../../../../shared/models/videos/video-streaming-playlist.type'
-import { sha256 } from '../../../helpers/core-utils'
 import { join } from 'path'
 
 const expect = chai.expect
@@ -56,19 +54,15 @@ async function checkHlsPlaylist (servers: ServerInfo[], videoUUID: string) {
         const res2 = await getPlaylist(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}.m3u8`)
 
         const subPlaylist = res2.text
-        expect(subPlaylist).to.contain(resolution + '_000.ts')
+        expect(subPlaylist).to.contain(`${videoUUID}-${resolution}-fragmented.mp4`)
       }
     }
 
     {
-      for (const resolution of resolutions) {
-
-        const res2 = await getSegment(`http://localhost:9001/static/playlists/hls/${videoUUID}/${resolution}_000.ts`)
+      const baseUrl = 'http://localhost:9001/static/playlists/hls'
 
-        const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
-
-        const sha256Server = resSha.body[ resolution + '_000.ts' ]
-        expect(sha256(res2.body)).to.equal(sha256Server)
+      for (const resolution of resolutions) {
+        await checkSegmentHash(baseUrl, baseUrl, videoUUID, resolution, hlsPlaylist)
       }
     }
   }
index dfec0bb76fd22a5b8d9050df7c7b6a9d0bd756aa..3de0890bb48f22bbe1e629a46bc2780ae7f18b69 100644 (file)
@@ -2,6 +2,9 @@ export interface ActivityPubOrderedCollection<T> {
   '@context': string[]
   type: 'OrderedCollection' | 'OrderedCollectionPage'
   totalItems: number
-  partOf?: string
   orderedItems: T[]
+
+  partOf?: string
+  next?: string
+  first?: string
 }
index fc687c70117e20960df1ff7f58ce7deb4fb7cb21..6b59e24fc188354b9e032377f8e4572bb7a81887 100644 (file)
@@ -3,10 +3,10 @@ import { buildAbsoluteFixturePath, root } from '../miscs/miscs'
 import { isAbsolute, join } from 'path'
 import { parse } from 'url'
 
-function makeRawRequest (url: string, statusCodeExpected?: number) {
+function makeRawRequest (url: string, statusCodeExpected?: number, range?: string) {
   const { host, protocol, pathname } = parse(url)
 
-  return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected })
+  return makeGetRequest({ url: `${protocol}//${host}`, path: pathname, statusCodeExpected, range })
 }
 
 function makeGetRequest (options: {
@@ -15,7 +15,8 @@ function makeGetRequest (options: {
   query?: any,
   token?: string,
   statusCodeExpected?: number,
-  contentType?: string
+  contentType?: string,
+  range?: string
 }) {
   if (!options.statusCodeExpected) options.statusCodeExpected = 400
   if (options.contentType === undefined) options.contentType = 'application/json'
@@ -25,6 +26,7 @@ function makeGetRequest (options: {
   if (options.contentType) req.set('Accept', options.contentType)
   if (options.token) req.set('Authorization', 'Bearer ' + options.token)
   if (options.query) req.query(options.query)
+  if (options.range) req.set('Range', options.range)
 
   return req.expect(options.statusCodeExpected)
 }
index 9a0710ca638534faf7e40f76c1f262e665d8c905..eb25011cbcb3a5381841c8f4b27c5454e8ea046e 100644 (file)
@@ -1,21 +1,51 @@
 import { makeRawRequest } from '../requests/requests'
+import { sha256 } from '../../../server/helpers/core-utils'
+import { VideoStreamingPlaylist } from '../../models/videos/video-streaming-playlist.model'
+import { expect } from 'chai'
 
 function getPlaylist (url: string, statusCodeExpected = 200) {
   return makeRawRequest(url, statusCodeExpected)
 }
 
-function getSegment (url: string, statusCodeExpected = 200) {
-  return makeRawRequest(url, statusCodeExpected)
+function getSegment (url: string, statusCodeExpected = 200, range?: string) {
+  return makeRawRequest(url, statusCodeExpected, range)
 }
 
 function getSegmentSha256 (url: string, statusCodeExpected = 200) {
   return makeRawRequest(url, statusCodeExpected)
 }
 
+async function checkSegmentHash (
+  baseUrlPlaylist: string,
+  baseUrlSegment: string,
+  videoUUID: string,
+  resolution: number,
+  hlsPlaylist: VideoStreamingPlaylist
+) {
+  const res = await getPlaylist(`${baseUrlPlaylist}/${videoUUID}/${resolution}.m3u8`)
+  const playlist = res.text
+
+  const videoName = `${videoUUID}-${resolution}-fragmented.mp4`
+
+  const matches = /#EXT-X-BYTERANGE:(\d+)@(\d+)/.exec(playlist)
+
+  const length = parseInt(matches[1], 10)
+  const offset = parseInt(matches[2], 10)
+  const range = `${offset}-${offset + length - 1}`
+
+  const res2 = await getSegment(`${baseUrlSegment}/${videoUUID}/${videoName}`, 206, `bytes=${range}`)
+
+  const resSha = await getSegmentSha256(hlsPlaylist.segmentsSha256Url)
+
+  const sha256Server = resSha.body[ videoName ][range]
+  expect(sha256(res2.body)).to.equal(sha256Server)
+}
+
 // ---------------------------------------------------------------------------
 
 export {
   getPlaylist,
   getSegment,
-  getSegmentSha256
+  getSegmentSha256,
+  checkSegmentHash
 }
index 47c0646e4f0b6472c20fca5a31e7ff583f30a579..1e759af1b6a18587d9f12d91368217dc1c77de8c 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -2,14 +2,6 @@
 # yarn lockfile v1
 
 
-"@babel/polyfill@^7.2.5":
-  version "7.2.5"
-  resolved "https://registry.yarnpkg.com/@babel/polyfill/-/polyfill-7.2.5.tgz#6c54b964f71ad27edddc567d065e57e87ed7fa7d"
-  integrity sha512-8Y/t3MWThtMLYr0YNC/Q76tqN1w30+b0uQMeFUYauG2UGTR19zyUtFrAzT23zNtBxPp+LbE5E/nwV/q/r3y6ug==
-  dependencies:
-    core-js "^2.5.7"
-    regenerator-runtime "^0.12.0"
-
 "@iamstarkov/listr-update-renderer@0.4.1":
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e"
@@ -3593,17 +3585,6 @@ hide-powered-by@1.0.0:
   resolved "https://registry.yarnpkg.com/hide-powered-by/-/hide-powered-by-1.0.0.tgz#4a85ad65881f62857fc70af7174a1184dccce32b"
   integrity sha1-SoWtZYgfYoV/xwr3F0oRhNzM4ys=
 
-"hlsdownloader@https://github.com/Chocobozzz/hlsdownloader#build":
-  version "0.0.0-semantic-release"
-  resolved "https://github.com/Chocobozzz/hlsdownloader#e19f9d803dcfe7ec25fd734b4743184f19a9b0cc"
-  dependencies:
-    "@babel/polyfill" "^7.2.5"
-    async "^2.6.1"
-    minimist "^1.2.0"
-    mkdirp "^0.5.1"
-    request "^2.88.0"
-    request-promise "^4.2.2"
-
 hosted-git-info@^2.1.4, hosted-git-info@^2.6.0, hosted-git-info@^2.7.1:
   version "2.7.1"
   resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.7.1.tgz#97f236977bd6e125408930ff6de3eec6281ec047"
@@ -4870,7 +4851,7 @@ lodash@=3.10.1:
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
   integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=
 
-lodash@^4.0.0, lodash@^4.13.1, lodash@^4.17.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.8.2, lodash@~4.17.10:
+lodash@^4.0.0, lodash@^4.17.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.8.2, lodash@~4.17.10:
   version "4.17.11"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
   integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
@@ -6651,11 +6632,6 @@ psl@^1.1.24:
   resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.29.tgz#60f580d360170bb722a797cc704411e6da850c67"
   integrity sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==
 
-psl@^1.1.28:
-  version "1.1.31"
-  resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
-  integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
-
 pstree.remy@^1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/pstree.remy/-/pstree.remy-1.1.2.tgz#4448bbeb4b2af1fed242afc8dc7416a6f504951a"
@@ -6699,7 +6675,7 @@ punycode@^1.4.1:
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
   integrity sha1-wNWmOycYgArY4esPpSachN1BhF4=
 
-punycode@^2.1.0, punycode@^2.1.1:
+punycode@^2.1.0:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
   integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
@@ -6982,11 +6958,6 @@ reflect-metadata@^0.1.12:
   resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.12.tgz#311bf0c6b63cd782f228a81abe146a2bfa9c56f2"
   integrity sha512-n+IyV+nGz3+0q3/Yf1ra12KpCyi001bi4XFxSjbiWWjfqb52iTTtpGXmCCAOWWIAn9KEuFZKGqBERHmrtScZ3A==
 
-regenerator-runtime@^0.12.0:
-  version "0.12.1"
-  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
-  integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
-
 regex-not@^1.0.0, regex-not@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/regex-not/-/regex-not-1.0.2.tgz#1f4ece27e00b0b65e0247a6810e6a85d83a5752c"
@@ -7036,23 +7007,6 @@ repeat-string@^1.6.1:
   resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
   integrity sha1-jcrkcOHIirwtYA//Sndihtp15jc=
 
-request-promise-core@1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.1.tgz#3eee00b2c5aa83239cfb04c5700da36f81cd08b6"
-  integrity sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=
-  dependencies:
-    lodash "^4.13.1"
-
-request-promise@^4.2.2:
-  version "4.2.2"
-  resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.2.tgz#d1ea46d654a6ee4f8ee6a4fea1018c22911904b4"
-  integrity sha1-0epG1lSm7k+O5qT+oQGMIpEZBLQ=
-  dependencies:
-    bluebird "^3.5.0"
-    request-promise-core "1.1.1"
-    stealthy-require "^1.1.0"
-    tough-cookie ">=2.3.3"
-
 request@^2.74.0, request@^2.81.0, request@^2.83.0, request@^2.87.0, request@^2.88.0:
   version "2.88.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
@@ -7970,11 +7924,6 @@ statuses@~1.4.0:
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
   integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew==
 
-stealthy-require@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b"
-  integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=
-
 stream-each@^1.1.0:
   version "1.2.3"
   resolved "https://registry.yarnpkg.com/stream-each/-/stream-each-1.2.3.tgz#ebe27a0c389b04fbcc233642952e10731afa9bae"
@@ -8467,15 +8416,6 @@ touch@^3.1.0:
   dependencies:
     nopt "~1.0.10"
 
-tough-cookie@>=2.3.3:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-3.0.1.tgz#9df4f57e739c26930a018184887f4adb7dca73b2"
-  integrity sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==
-  dependencies:
-    ip-regex "^2.1.0"
-    psl "^1.1.28"
-    punycode "^2.1.1"
-
 tough-cookie@~2.4.3:
   version "2.4.3"
   resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"