├── data-structure.png ├── test ├── fixtures │ ├── m3u8 │ │ ├── 8.1-Simple-Media-Playlist.m3u8 │ │ ├── SCTE-35_01.m3u8 │ │ ├── 8.11-EXT-X-CUE-OUT-Media-Playlist.m3u8 │ │ ├── SCTE-35_03.m3u8 │ │ ├── 8.2-Live-Media-Playlist_using-HTTPS.m3u8 │ │ ├── RedundantSegments.m3u8 │ │ ├── 8.8-Session-Data-in-a-Master-Playlist.m3u8 │ │ ├── SCTE-35_02.m3u8 │ │ ├── SCTE-35_05.m3u8 │ │ ├── 8.4-Master-Playlist.m3u8 │ │ ├── SCTE-35_04.m3u8 │ │ ├── 8.3-Playlist-with-encrypted-Media-Segments.m3u8 │ │ ├── SCTE-35_06.m3u8 │ │ ├── 8.5-Master-Playlist-with-I-Frames.m3u8 │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-01.m3u8 │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-02.m3u8 │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-03.m3u8 │ │ ├── Multiple-rendition-groups.m3u8 │ │ ├── SCTE-35_07.m3u8 │ │ ├── 8.6-Master-Playlist-with-Alternative-audio.m3u8 │ │ ├── 8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.m3u8 │ │ ├── 8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.m3u8 │ │ ├── 8.7-Master-Playlist-with-Alternative-video.m3u8 │ │ ├── Low-Latency_Example-02_Playlist_Delta_Update.m3u8 │ │ ├── Low-Latency_Example-01_Low-Latency_HLS_Playlist.m3u8 │ │ ├── Streaming-Examples_bipbop_16x9_variant.m3u8 │ │ └── Streaming-Examples_img_bipbop_adv_example_ts_master.m3u8 │ └── objects │ │ ├── 8.8-Session-Data-in-a-Master-Playlist.js │ │ ├── 8.1-Simple-Media-Playlist.js │ │ ├── 8.4-Master-Playlist.js │ │ ├── 8.11-EXT-X-CUE-OUT-Media-Playlist.js │ │ ├── 8.2-Live-Media-Playlist_using-HTTPS.js │ │ ├── RedundantSegments.js │ │ ├── 8.5-Master-Playlist-with-I-Frames.js │ │ ├── SCTE-35_01.js │ │ ├── SCTE-35_03.js │ │ ├── Multiple-rendition-groups.js │ │ ├── 8.3-Playlist-with-encrypted-Media-Segments.js │ │ ├── SCTE-35_05.js │ │ ├── SCTE-35_02.js │ │ ├── 8.7-Master-Playlist-with-Alternative-video.js │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-01.js │ │ ├── SCTE-35_04.js │ │ ├── 8.6-Master-Playlist-with-Alternative-audio.js │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-02.js │ │ ├── SCTE-35_07.js │ │ ├── Low-Latency_Example-03_Byterange-addressed_Parts-03.js │ │ ├── 8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.js │ │ ├── 8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.js │ │ ├── SCTE-35_06.js │ │ ├── Low-Latency_Example-02_Playlist_Delta_Update.js │ │ ├── Low-Latency_Example-01_Low-Latency_HLS_Playlist.js │ │ └── Streaming-Examples_bipbop_16x9_variant.js ├── spec │ ├── 4_Playlists │ │ └── 4.3_Playlist-Tags │ │ │ ├── 4.3.4_Master-Playlist-Tags │ │ │ ├── 4.3.4.3_EXT-X-I-FRAME-STREAM-INF.spec.js │ │ │ ├── 4.3.4.2_EXT-X-STREAM-INF_2.spec.js │ │ │ ├── 4.3.4_Master-Playlist-Tags.spec.js │ │ │ ├── 4.3.4.5_EXT-X-SESSION-KEY.spec.js │ │ │ ├── 4.3.4.4_EXT-X-SESSION-DATA.spec.js │ │ │ └── 4.3.4.1_EXT-X-MEDIA.spec.js │ │ │ ├── 4.3.2_Media-Segment-Tags │ │ │ ├── 4.3.2.6_EXT-X-PROGRAM-DATE-TIME.spec.js │ │ │ ├── 4.3.2.3_EXT-X-DISCONTINUITY.spec.js │ │ │ ├── 4.3.2_Media-Segment-Tags.spec.js │ │ │ ├── 4.4.4.7_EXT-X-GAP.spec.js │ │ │ ├── 4.3.2.1_EXTINF.spec.js │ │ │ ├── 4.3.2.2_EXT-X-BYTERANGE.spec.js │ │ │ ├── 4.3.2.5_EXT-X-MAP.spec.js │ │ │ └── 4.3.2.4_EXT-X-KEY.spec.js │ │ │ ├── 4.3.1_Basic-Tags │ │ │ ├── 4.3.1.2_EXT-X-VERSION.spec.js │ │ │ └── 4.3.1.1_EXTM3U.spec.js │ │ │ ├── 4.3.3_Media-Playlist-Tags │ │ │ ├── 4.3.3.6_EXT-X-I-FRAMES-ONLY.spec.js │ │ │ ├── 4.3.3.7_EXT-X-CUE-OUT.spec.js │ │ │ ├── 4.3.3.5_EXT-X-PLAYLIST-TYPE.spec.js │ │ │ ├── 4.3.3.1_EXT-X-TARGETDURATION.spec.js │ │ │ ├── 4.3.3_Media-Playlist-Tags.spec.js │ │ │ ├── 4.3.3.4_EXT-X-ENDLIST.spec.js │ │ │ ├── 4.3.3.2_EXT-X-MEDIA-SEQUENCE.spec.js │ │ │ └── 4.3.3.3_EXT-X-DISCONTINUITY-SEQUENCE.spec.js │ │ │ └── 4.3.5_Media-or-Master-Playlist-Tags │ │ │ ├── 4.3.5.1_EXT-X-INDEPENDENT-SEGMENTS.spec.js │ │ │ └── 4.3.5.2_EXT-X-START.spec.js │ ├── Apple_HLS_Overview │ │ └── 02_Using_HLS.spec.js │ ├── HLSJS-LHLS │ │ ├── 02_EXT-X-PREFETCH-DISCONTINUITY.spec.js │ │ └── 01_EXT-X-PREFETCH.spec.js │ ├── misc │ │ ├── multiple-rendition-groups.js │ │ └── scte-35.spec.js │ ├── Apple-Low-Latency │ │ └── New_Media_Playlist_Tags_for_Low-Latency_HLS │ │ │ ├── 06_EXT-X-SKIP.spec.js │ │ │ ├── 05_EXT-X-RENDITION-REPORT.spec.js │ │ │ ├── 02_EXT-X-PART-INF.spec.js │ │ │ ├── 01_EXT-X-SERVER-CONTROL.spec.js │ │ │ └── 04_EXT-X-PRELOAD-HINT.spec.js │ ├── utils.spec.js │ └── 7_Protocol-version-compatibility │ │ └── 7_EXT-X-VERSION.spec.js └── helpers │ ├── fixtures.js │ ├── matchers.js │ └── utils.js ├── tsconfig.json ├── index.ts ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── .npmignore ├── webpack.config.js ├── LICENSE ├── package.json └── utils.ts /data-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kuu/hls-parser/HEAD/data-structure.png -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.1-Simple-Media-Playlist.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:10 4 | #EXTINF:9.009, 5 | http://media.example.com/first.ts 6 | #EXTINF:9.009, 7 | http://media.example.com/second.ts 8 | #EXTINF:3.003, 9 | http://media.example.com/third.ts 10 | #EXT-X-ENDLIST 11 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/SCTE-35_01.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:8 4 | #EXT-X-PLAYLIST-TYPE:VOD 5 | #EXTINF:8.008, 6 | 1.ts 7 | #EXT-X-CUE-OUT:DURATION=15 8 | #EXTINF:8, 9 | 2.ts 10 | #EXTINF:7, 11 | 3.ts 12 | #EXT-X-CUE-IN 13 | #EXTINF:8.008, 14 | 4.ts 15 | #EXTINF:8.008, 16 | 5.ts 17 | #EXT-X-ENDLIST 18 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.11-EXT-X-CUE-OUT-Media-Playlist.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:10 4 | #EXTINF:9.009, 5 | http://media.example.com/first.ts 6 | #EXT-X-CUE-OUT:DURATION=15 7 | #EXTINF:9.009, 8 | http://media.example.com/second.ts 9 | #EXTINF:3.003, 10 | http://media.example.com/third.ts 11 | #EXT-X-ENDLIST 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "module": "node", 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "checkJs": true, 7 | "declaration": true, 8 | "noImplicitAny": false 9 | }, 10 | "exclude": [ 11 | "webpack.config.js", 12 | "dist", 13 | "test" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | /*! Copyright Kuu Miyazaki. SPDX-License-Identifier: MIT */ 2 | import { getOptions, setOptions } from './utils'; 3 | import parse from './parse'; 4 | import stringify from './stringify'; 5 | import * as types from './types'; 6 | 7 | export { 8 | parse, 9 | stringify, 10 | types, 11 | getOptions, 12 | setOptions 13 | }; 14 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/SCTE-35_03.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:8 4 | #EXT-X-PLAYLIST-TYPE:VOD 5 | #EXTINF:8.008, 6 | 1.ts 7 | #EXT-X-CUE:DURATION="15.0",ID="0",TYPE="SpliceOut",TIME="414.171" 8 | #EXTINF:8, 9 | 2.ts 10 | #EXTINF:7, 11 | 3.ts 12 | #EXTINF:8.008, 13 | 4.ts 14 | #EXTINF:8.008, 15 | 5.ts 16 | #EXT-X-ENDLIST 17 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.2-Live-Media-Playlist_using-HTTPS.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:8 4 | #EXT-X-MEDIA-SEQUENCE:2680 5 | 6 | #EXTINF:7.975, 7 | https://priv.example.com/fileSequence2680.ts 8 | #EXTINF:7.941, 9 | https://priv.example.com/fileSequence2681.ts 10 | #EXTINF:7.975, 11 | https://priv.example.com/fileSequence2682.ts 12 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/RedundantSegments.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:4 3 | #EXT-X-TARGETDURATION:10 4 | #EXTINF:9.009, 5 | http://media.example.com/first.ts 6 | #EXTINF:9.009, 7 | http://media.example.com/second.ts 8 | #EXTINF:9.009, 9 | #EXT-X-BYTERANGE:128@256 10 | http://media.example.com/second.ts 11 | #EXTINF:3.003, 12 | http://media.example.com/third.ts 13 | #EXT-X-ENDLIST 14 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.8-Session-Data-in-a-Master-Playlist.m3u8: -------------------------------------------------------------------------------- 1 | # In this example, only the EXT-X-SESSION-DATA is shown: 2 | #EXTM3U 3 | 4 | #EXT-X-SESSION-DATA:DATA-ID="com.example.lyrics",URI="lyrics.json" 5 | 6 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" 7 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="es",VALUE="Este es un ejemplo" 8 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/SCTE-35_02.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:8 4 | #EXT-X-PLAYLIST-TYPE:VOD 5 | #EXTINF:8.008, 6 | 1.ts 7 | #EXT-X-CUE-OUT:DURATION=23 8 | #EXTINF:8, 9 | 2.ts 10 | #EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23 11 | #EXTINF:8, 12 | 3.ts 13 | #EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23 14 | #EXTINF:7, 15 | 4.ts 16 | #EXT-X-CUE-IN 17 | #EXTINF:8.008, 18 | 5.ts 19 | #EXT-X-ENDLIST 20 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/SCTE-35_05.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:8 4 | #EXT-X-PLAYLIST-TYPE:VOD 5 | #EXTINF:8.008, 6 | 1.ts 7 | #EXT-X-SCTE35:TYPE=0x34,DURATION=15.0,CUE-OUT=YES,UPID="0x08:0x9425BC",CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”pIViS5” 8 | #EXTINF:8, 9 | 2.ts 10 | #EXTINF:7, 11 | 3.ts 12 | #EXT-X-SCTE35:TYPE=0x35,CUE-IN=YES,CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”f6UrRd” 13 | #EXTINF:8.008, 14 | 4.ts 15 | #EXTINF:8.008, 16 | 5.ts 17 | #EXT-X-ENDLIST 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: HLS parser tests 2 | on: [ push ] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | strategy: 7 | matrix: 8 | node-version: [ 'current', 'lts/*', 'lts/-1' ] 9 | steps: 10 | - uses: actions/checkout@v4 11 | - name: Setup Node ${{ matrix.node-version }} 12 | uses: actions/setup-node@v4 13 | with: 14 | node-version: ${{ matrix.node-version }} 15 | - run: npm ci 16 | - run: npm test 17 | - run: npm run build 18 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.4-Master-Playlist.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AVERAGE-BANDWIDTH=1000000,CODECS="avc1.640029,mp4a.40.2" 3 | http://example.com/low.m3u8 4 | #EXT-X-STREAM-INF:BANDWIDTH=2560000,AVERAGE-BANDWIDTH=2000000,CODECS="avc1.640029,mp4a.40.2" 5 | http://example.com/mid.m3u8 6 | #EXT-X-STREAM-INF:BANDWIDTH=7680000,AVERAGE-BANDWIDTH=6000000,CODECS="avc1.640029,mp4a.40.2" 7 | http://example.com/hi.m3u8 8 | #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" 9 | http://example.com/audio-only.m3u8 10 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/SCTE-35_04.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:8 4 | #EXT-X-PLAYLIST-TYPE:VOD 5 | #EXTINF:8.008, 6 | 1.ts 7 | #EXT-OATCLS-SCTE35:/DA0AAAAAAAAAAAABQb+ADAQ6QAeAhxDVUVJQAAAO3/PAAEUrEoICAAAAAAg+2UBNAAANvrtoQ== 8 | #EXT-X-ASSET:CAID=0x0000000020FB6501 9 | #EXT-X-CUE-OUT:DURATION=15 10 | #EXTINF:8, 11 | 2.ts 12 | #EXT-X-CUE-OUT-CONT:ElapsedTime=5.939,Duration=25.0,SCTE35=/DA0AAAA+…AAg+2UBNAAANvrtoQ== 13 | #EXTINF:7, 14 | 3.ts 15 | #EXT-X-CUE-IN 16 | #EXTINF:8.008, 17 | 4.ts 18 | #EXTINF:8.008, 19 | 5.ts 20 | #EXT-X-ENDLIST 21 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.3_EXT-X-I-FRAME-STREAM-INF.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // Every EXT-X-I-FRAME-STREAM-INF tag MUST include a BANDWIDTH attribute 5 | // and a URI attribute. 6 | test('#EXT-X-I-FRAME-STREAM-INF_01', t => { 7 | utils.parseFail(t, ` 8 | #EXTM3U 9 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1280000 10 | `); 11 | utils.bothPass(t, ` 12 | #EXTM3U 13 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=1280000,URI=/video/main.m3u8 14 | `); 15 | }); 16 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.3-Playlist-with-encrypted-Media-Segments.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:15 4 | #EXT-X-MEDIA-SEQUENCE:7794 5 | 6 | #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=52" 7 | 8 | #EXTINF:2.833, 9 | http://media.example.com/fileSequence52-A.ts 10 | #EXTINF:15, 11 | http://media.example.com/fileSequence52-B.ts 12 | #EXTINF:13.333, 13 | http://media.example.com/fileSequence52-C.ts 14 | 15 | #EXT-X-KEY:METHOD=AES-128,URI="https://priv.example.com/key.php?r=53" 16 | 17 | #EXTINF:15, 18 | http://media.example.com/fileSequence53-A.ts 19 | -------------------------------------------------------------------------------- /test/helpers/fixtures.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | const fs = require('node:fs'); 3 | 4 | const fixtures = []; 5 | const baseDir = path.join(__dirname, '../fixtures/m3u8'); 6 | const filenames = fs.readdirSync(baseDir); 7 | 8 | for (const filename of filenames) { 9 | if (filename.endsWith('.m3u8')) { 10 | const name = path.basename(filename, '.m3u8'); 11 | const filepath = path.join(baseDir, filename); 12 | const m3u8 = fs.readFileSync(filepath, 'utf8'); 13 | const object = require(`../fixtures/objects/${name}.js`); 14 | fixtures.push({name, m3u8, object}); 15 | } 16 | } 17 | 18 | module.exports = fixtures; 19 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.6_EXT-X-PROGRAM-DATE-TIME.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | 4 | // It applies only to the next Media Segment. 5 | test('#EXT-X-PROGRAM-DATE-TIME_01', t => { 6 | const playlist = HLS.parse(` 7 | #EXTM3U 8 | #EXT-X-TARGETDURATION:10 9 | #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00 10 | #EXTINF:10, 11 | http://example.com/1 12 | #EXTINF:10, 13 | http://example.com/2 14 | `); 15 | t.truthy(playlist.segments[0].programDateTime); 16 | t.falsy(playlist.segments[1].programDateTime); 17 | }); 18 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/SCTE-35_06.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:3 3 | #EXT-X-TARGETDURATION:8 4 | #EXT-X-PLAYLIST-TYPE:VOD 5 | #EXTINF:8.008, 6 | 1.ts 7 | #EXT-X-CUE-OUT:DURATION=23 8 | #EXTINF:8, 9 | 2.ts 10 | #EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23 11 | #EXTINF:8, 12 | 3.ts 13 | #EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23 14 | #EXTINF:7, 15 | 4.ts 16 | #EXT-X-CUE-IN 17 | #EXTINF:8.008, 18 | 5.ts 19 | #EXT-X-CUE-OUT:DURATION=23 20 | #EXTINF:8, 21 | 6.ts 22 | #EXT-X-CUE-OUT-CONT:ElapsedTime=8,Duration=23 23 | #EXTINF:8, 24 | 7.ts 25 | #EXT-X-CUE-OUT-CONT:ElapsedTime=16,Duration=23 26 | #EXTINF:7, 27 | 8.ts 28 | #EXT-X-CUE-IN 29 | #EXTINF:8.008, 30 | 9.ts 31 | #EXT-X-ENDLIST 32 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.5-Master-Playlist-with-I-Frames.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="avc1.640029,mp4a.40.2" 3 | low/audio-video.m3u8 4 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=86000,URI="low/iframe.m3u8",CODECS="avc1.640029" 5 | #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="avc1.640029,mp4a.40.2" 6 | mid/audio-video.m3u8 7 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=150000,URI="mid/iframe.m3u8",CODECS="avc1.640029" 8 | #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="avc1.640029,mp4a.40.2" 9 | hi/audio-video.m3u8 10 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=550000,URI="hi/iframe.m3u8",CODECS="avc1.640029" 11 | #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5" 12 | audio-only.m3u8 13 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.2_EXT-X-STREAM-INF_2.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | const utils = require('../../../../helpers/utils'); 4 | 5 | test('#EXT-X-STREAM-INF_07-03', t => { 6 | const sourceText = ` 7 | #EXTM3U 8 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS=NONE 9 | /video/main.m3u8 10 | #EXT-X-STREAM-INF:BANDWIDTH=2040000,CLOSED-CAPTIONS=NONE 11 | /video/high.m3u8 12 | `; 13 | HLS.setOptions({allowClosedCaptionsNone: true}); 14 | const obj = HLS.parse(sourceText); 15 | const text = HLS.stringify(obj); 16 | t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); 17 | }); 18 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-01.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:9 3 | #EXT-X-TARGETDURATION:4 4 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1.02 5 | #EXT-X-PART-INF:PART-TARGET=1.02 6 | #EXT-X-MEDIA-SEQUENCE:266 7 | #EXT-X-SKIP:SKIPPED-SEGMENTS=3 8 | #EXTINF:4.00008, 9 | fileSequence269.mp4 10 | #EXTINF:4.00008, 11 | fileSequence270.mp4 12 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=20000@0 13 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=23000@20000 14 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=18000@43000 15 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence271.mp4",BYTERANGE-START=61000 16 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.1_Basic-Tags/4.3.1.2_EXT-X-VERSION.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // A Playlist file MUST NOT contain more than one EXT-X-VERSION tag. 5 | test('#EXT-X-VERSION_01', t => { 6 | utils.bothPass(t, ` 7 | #EXTM3U 8 | #EXT-X-VERSION:3 9 | #EXT-X-TARGETDURATION:10 10 | #EXTINF:9.9, 11 | http://example.com/1 12 | #EXTINF:10.0, 13 | http://example.com/2 14 | `); 15 | utils.parseFail(t, ` 16 | #EXTM3U 17 | #EXT-X-VERSION:3 18 | #EXT-X-TARGETDURATION:10 19 | #EXTINF:9.9, 20 | http://example.com/1 21 | #EXTINF:10.0, 22 | http://example.com/2 23 | #EXT-X-VERSION:4 24 | `); 25 | }); 26 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4_Master-Playlist-Tags.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // Master Playlist Tags MUST NOT appear in a Media Playlist 5 | test('Master-Playlist-Tags', t => { 6 | utils.bothPass(t, ` 7 | #EXTM3U 8 | #EXT-X-TARGETDURATION:10 9 | #EXTINF:10, 10 | http://example.com/1 11 | #EXTINF:10, 12 | http://example.com/2 13 | `); 14 | utils.parseFail(t, ` 15 | #EXTM3U 16 | #EXT-X-TARGETDURATION:10 17 | #EXTINF:10, 18 | http://example.com/1 19 | #EXTINF:10, 20 | http://example.com/2 21 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" 22 | `); 23 | }); 24 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.3_EXT-X-DISCONTINUITY.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | 4 | // The EXT-X-DISCONTINUITY tag indicates a discontinuity between the 5 | // Media Segment that follows it and the one that preceded it. 6 | test('#EXT-X-DISCONTINUITY_01', t => { 7 | const playlist = HLS.parse(` 8 | #EXTM3U 9 | #EXT-X-TARGETDURATION:10 10 | #EXTINF:10, 11 | http://example.com/1 12 | #EXT-X-DISCONTINUITY 13 | #EXTINF:10, 14 | http://example.com/2 15 | #EXTINF:10, 16 | http://example.com/3 17 | `); 18 | t.falsy(playlist.segments[0].discontinuity); 19 | t.true(playlist.segments[1].discontinuity); 20 | t.falsy(playlist.segments[2].discontinuity); 21 | }); 22 | -------------------------------------------------------------------------------- /test/fixtures/objects/8.8-Session-Data-in-a-Master-Playlist.js: -------------------------------------------------------------------------------- 1 | const {MasterPlaylist, SessionData} = require('../../../types'); 2 | 3 | const playlist = new MasterPlaylist({ 4 | sessionDataList: createSetssionDataList() 5 | }); 6 | 7 | function createSetssionDataList() { 8 | const setssionDataList = []; 9 | setssionDataList.push(new SessionData({ 10 | id: 'com.example.lyrics', 11 | uri: 'lyrics.json' 12 | })); 13 | setssionDataList.push(new SessionData({ 14 | id: 'com.example.title', 15 | language: 'en', 16 | value: 'This is an example' 17 | })); 18 | setssionDataList.push(new SessionData({ 19 | id: 'com.example.title', 20 | language: 'es', 21 | value: 'Este es un ejemplo' 22 | })); 23 | return setssionDataList; 24 | } 25 | 26 | module.exports = playlist; 27 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.6_EXT-X-I-FRAMES-ONLY.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // Use of the EXT-X-I-FRAMES-ONLY REQUIRES a compatibility version 5 | // number of 4 or greater. 6 | test('#EXT-X-I-FRAMES-ONLY_01', t => { 7 | utils.parseFail(t, ` 8 | #EXTM3U 9 | #EXT-X-TARGETDURATION:10 10 | #EXT-X-VERSION:3 11 | #EXT-X-I-FRAMES-ONLY 12 | #EXTINF:9, 13 | http://example.com/1 14 | #EXTINF:10, 15 | http://example.com/2 16 | `); 17 | utils.bothPass(t, ` 18 | #EXTM3U 19 | #EXT-X-TARGETDURATION:10 20 | #EXT-X-VERSION:4 21 | #EXT-X-I-FRAMES-ONLY 22 | #EXTINF:9, 23 | http://example.com/1 24 | #EXTINF:10, 25 | http://example.com/2 26 | `); 27 | }); 28 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-02.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:9 3 | #EXT-X-TARGETDURATION:4 4 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1.02 5 | #EXT-X-PART-INF:PART-TARGET=1.02 6 | #EXT-X-MEDIA-SEQUENCE:266 7 | #EXT-X-SKIP:SKIPPED-SEGMENTS=3 8 | #EXTINF:4.00008, 9 | fileSequence269.mp4 10 | #EXTINF:4.00008, 11 | fileSequence270.mp4 12 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=20000@0 13 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=23000@20000 14 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=18000@43000 15 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=19000@61000 16 | #EXTINF:4.00008, 17 | fileSequence271.mp4 18 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence272.mp4",BYTERANGE-START=0 19 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.7_EXT-X-CUE-OUT.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | test('#EXT-X-CUE-OUT_01', t => { 5 | let obj = utils.parsePass(t, ` 6 | #EXTM3U 7 | #EXT-X-TARGETDURATION:10 8 | #EXT-X-VERSION:3 9 | #EXTINF:9, 10 | http://example.com/1 11 | #EXT-X-CUE-OUT:30 12 | #EXTINF:10, 13 | http://example.com/2 14 | `); 15 | t.is(obj.segments[1].markers[0].duration, 30); 16 | obj = utils.parsePass(t, ` 17 | #EXTM3U 18 | #EXT-X-TARGETDURATION:10 19 | #EXT-X-VERSION:3 20 | #EXTINF:9, 21 | http://example.com/1 22 | #EXT-X-CUE-OUT:DURATION=30 23 | #EXTINF:10, 24 | http://example.com/2 25 | `); 26 | t.is(obj.segments[1].markers[0].duration, 30); 27 | }); 28 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/Low-Latency_Example-03_Byterange-addressed_Parts-03.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:9 3 | #EXT-X-TARGETDURATION:4 4 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1.02 5 | #EXT-X-PART-INF:PART-TARGET=1.02 6 | #EXT-X-MEDIA-SEQUENCE:266 7 | #EXT-X-SKIP:SKIPPED-SEGMENTS=3 8 | #EXTINF:4.00008, 9 | fileSequence269.mp4 10 | #EXTINF:4.00008, 11 | fileSequence270.mp4 12 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=20000@0 13 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=23000@20000 14 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=18000@43000 15 | #EXT-X-PART:DURATION=1.02,URI="fileSequence271.mp4",BYTERANGE=19000@61000 16 | #EXTINF:4.00008, 17 | fileSequence271.mp4 18 | #EXT-X-PART:DURATION=1.02,URI="fileSequence272.mp4",BYTERANGE=21000@0 19 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fileSequence272.mp4",BYTERANGE-START=21000 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Editors 27 | .idea/ 28 | .vscode/ 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Typescript intermediate files 34 | tsconfig.tsbuildinfo 35 | *.d.ts 36 | *.js 37 | !test/**/*.js 38 | 39 | # Dependency directories 40 | node_modules 41 | jspm_packages 42 | 43 | # Optional npm cache directory 44 | .npm 45 | 46 | # Optional REPL history 47 | .node_repl_history 48 | 49 | dist 50 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/Multiple-rendition-groups.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:4 3 | #EXT-X-INDEPENDENT-SEGMENTS 4 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="English",DEFAULT=YES,URI="aac_high_eng.m3u8" 5 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="Japanese",DEFAULT=NO,URI="aac_high_jp.m3u8" 6 | #EXT-X-STREAM-INF:BANDWIDTH=6000000,AUDIO="aac_high" 7 | 1080p.m3u8 8 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="English",DEFAULT=YES,URI="aac_mid_eng.m3u8" 9 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="Japanese",DEFAULT=NO,URI="aac_mid_jp.m3u8" 10 | #EXT-X-STREAM-INF:BANDWIDTH=3000000,AUDIO="aac_mid" 11 | 720p.m3u8 12 | #EXT-X-STREAM-INF:BANDWIDTH=1500000,AUDIO="aac_mid" 13 | 540p.m3u8 14 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="English",DEFAULT=YES,URI="aac_low_eng.m3u8" 15 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="Japanese",DEFAULT=NO,URI="aac_low_jp.m3u8" 16 | #EXT-X-STREAM-INF:BANDWIDTH=1000000,AUDIO="aac_low" 17 | 360p.m3u8 -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Editors 34 | .idea/ 35 | .vscode/ 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | test 44 | .github/ 45 | .node-version 46 | .travis.yml 47 | .npmignore 48 | data-structure.png 49 | tsconfig.tsbuildinfo 50 | webpack.config.js 51 | LICENSE 52 | *.ts 53 | !*.d.ts 54 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/SCTE-35_07.m3u8: -------------------------------------------------------------------------------- 1 | # This example shows two EXT-X-DATERANGE tags that describe a single 2 | # Date Range, with a SCTE-35 "out" splice_insert() command that is 3 | # subsequently updated with an SCTE-35 "in" splice_insert() command. 4 | 5 | #EXTM3U 6 | #EXT-X-VERSION:3 7 | #EXT-X-TARGETDURATION:30 8 | 9 | #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z 10 | #EXTINF:30, 11 | http://media.example.com/01.ts 12 | #EXTINF:30, 13 | http://media.example.com/02.ts 14 | #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2023-10-09T06:16:00.820Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC30250001D1F7E25300FFF0140565239AA07FEFFE015C3F90FE00526362000101010000A7C1792D 15 | #EXTINF:30, 16 | http://ads.example.com/ad-01.ts 17 | #EXTINF:30, 18 | http://ads.example.com/ad-02.ts 19 | #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2023-10-09T06:16:00.820Z",END-DATE="2023-10-09T06:17:01.514Z",DURATION=60.694 20 | #EXTINF:30, 21 | http://media.example.com/03.ts 22 | #EXTINF:3.003, 23 | http://media.example.com/04.ts 24 | -------------------------------------------------------------------------------- /test/fixtures/objects/8.1-Simple-Media-Playlist.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | version: 3, 5 | targetDuration: 10, 6 | segments: createSegments(), 7 | endlist: true 8 | }); 9 | 10 | function createSegments() { 11 | const segments = []; 12 | segments.push(new Segment({ 13 | uri: 'http://media.example.com/first.ts', 14 | duration: 9.009, 15 | title: '', 16 | mediaSequenceNumber: 0, 17 | discontinuitySequence: 0 18 | })); 19 | segments.push(new Segment({ 20 | uri: 'http://media.example.com/second.ts', 21 | duration: 9.009, 22 | title: '', 23 | mediaSequenceNumber: 1, 24 | discontinuitySequence: 0 25 | })); 26 | segments.push(new Segment({ 27 | uri: 'http://media.example.com/third.ts', 28 | duration: 3.003, 29 | title: '', 30 | mediaSequenceNumber: 2, 31 | discontinuitySequence: 0 32 | })); 33 | return segments; 34 | } 35 | 36 | module.exports = playlist; 37 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2_Media-Segment-Tags.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // A Media Segment tag MUST NOT appear in a Master Playlist. Clients 5 | // MUST reject Playlists that contain both Media Segment Tags and Master 6 | // Playlist tags. 7 | test('Media-Segment-Tags', t => { 8 | utils.bothPass(t, ` 9 | #EXTM3U 10 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 11 | http://example.com/low.m3u8 12 | #EXT-X-STREAM-INF:BANDWIDTH=2560000 13 | http://example.com/mid.m3u8 14 | #EXT-X-STREAM-INF:BANDWIDTH=7680000 15 | http://example.com/hi.m3u8 16 | `); 17 | utils.parseFail(t, ` 18 | #EXTM3U 19 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 20 | http://example.com/low.m3u8 21 | #EXT-X-STREAM-INF:BANDWIDTH=2560000 22 | http://example.com/mid.m3u8 23 | #EXT-X-DISCONTINUITY 24 | #EXT-X-STREAM-INF:BANDWIDTH=7680000 25 | http://example.com/hi.m3u8 26 | `); 27 | }); 28 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.6-Master-Playlist-with-Alternative-audio.m3u8: -------------------------------------------------------------------------------- 1 | # In this example, the CODECS attributes have been condensed for space. 2 | # A '\' is used to indicate that the tag continues on the following 3 | # line with whitespace removed: 4 | 5 | #EXTM3U 6 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",URI="main/english-audio.m3u8" 7 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de",URI="main/german-audio.m3u8" 8 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en",URI="commentary/audio-only.m3u8" 9 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="mp4a.40.2",AUDIO="aac" 10 | low/video-only.m3u8 11 | #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="mp4a.40.2",AUDIO="aac" 12 | mid/video-only.m3u8 13 | #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="mp4a.40.2",AUDIO="aac" 14 | hi/video-only.m3u8 15 | #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac" 16 | main/english-audio.m3u8 17 | -------------------------------------------------------------------------------- /test/fixtures/objects/8.4-Master-Playlist.js: -------------------------------------------------------------------------------- 1 | const {MasterPlaylist, Variant} = require('../../../types'); 2 | 3 | const playlist = new MasterPlaylist({ 4 | variants: createVariants() 5 | }); 6 | 7 | function createVariants() { 8 | const variants = []; 9 | variants.push(new Variant({ 10 | uri: 'http://example.com/low.m3u8', 11 | bandwidth: 1280000, 12 | averageBandwidth: 1000000, 13 | codecs: 'avc1.640029,mp4a.40.2' 14 | })); 15 | variants.push(new Variant({ 16 | uri: 'http://example.com/mid.m3u8', 17 | bandwidth: 2560000, 18 | averageBandwidth: 2000000, 19 | codecs: 'avc1.640029,mp4a.40.2' 20 | })); 21 | variants.push(new Variant({ 22 | uri: 'http://example.com/hi.m3u8', 23 | bandwidth: 7680000, 24 | averageBandwidth: 6000000, 25 | codecs: 'avc1.640029,mp4a.40.2' 26 | })); 27 | variants.push(new Variant({ 28 | uri: 'http://example.com/audio-only.m3u8', 29 | bandwidth: 65000, 30 | codecs: 'mp4a.40.5' 31 | })); 32 | return variants; 33 | } 34 | 35 | module.exports = playlist; 36 | -------------------------------------------------------------------------------- /test/spec/Apple_HLS_Overview/02_Using_HLS.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../..'); 3 | const utils = require('../../helpers/utils'); 4 | 5 | // Starting with iOS 3.1, if the client is unable to reload the index file for a stream (due to a 404 error, for example), 6 | // the client attempts to switch to an alternate stream. 7 | test('Redundant_Streams_01', t => { 8 | const sourceText = ` 9 | #EXTM3U 10 | #EXT-X-STREAM-INF:BANDWIDTH=200000, RESOLUTION=720x480 11 | http://ALPHA.mycompany.com/lo/prog_index.m3u8 12 | #EXT-X-STREAM-INF:BANDWIDTH=200000, RESOLUTION=720x480 13 | http://BETA.mycompany.com/lo/prog_index.m3u8 14 | 15 | #EXT-X-STREAM-INF:BANDWIDTH=500000, RESOLUTION=1920x1080 16 | http://ALPHA.mycompany.com/md/prog_index.m3u8 17 | #EXT-X-STREAM-INF:BANDWIDTH=500000, RESOLUTION=1920x1080 18 | http://BETA.mycompany.com/md/prog_index.m3u8 19 | `; 20 | const obj = HLS.parse(sourceText); 21 | const text = HLS.stringify(obj); 22 | t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); 23 | }); 24 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.m3u8: -------------------------------------------------------------------------------- 1 | # This example shows two EXT-X-DATERANGE tags that describe a single 2 | # Date Range, with a SCTE-35 "out" splice_insert() command that is 3 | # subsequently updated with an SCTE-35 "in" splice_insert() command. 4 | 5 | #EXTM3U 6 | #EXT-X-VERSION:3 7 | #EXT-X-TARGETDURATION:30 8 | 9 | #EXT-X-PROGRAM-DATE-TIME:2014-03-05T11:14:00.000Z 10 | #EXTINF:30, 11 | http://media.example.com/01.ts 12 | #EXTINF:30, 13 | http://media.example.com/02.ts 14 | #EXT-X-DATERANGE:ID="splice-6FFFFFF0",START-DATE="2014-03-05T11:15:00.000Z",PLANNED-DURATION=59.993,SCTE35-OUT=0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000 15 | #EXTINF:30, 16 | http://ads.example.com/ad-01.ts 17 | #EXTINF:30, 18 | http://ads.example.com/ad-02.ts 19 | #EXT-X-DATERANGE:ID="splice-6FFFFFF0",DURATION=59.993,SCTE35-IN=0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000 20 | #EXTINF:30, 21 | http://media.example.com/03.ts 22 | #EXTINF:3.003, 23 | http://media.example.com/04.ts 24 | -------------------------------------------------------------------------------- /test/fixtures/objects/8.11-EXT-X-CUE-OUT-Media-Playlist.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | version: 3, 5 | targetDuration: 10, 6 | segments: createSegments(), 7 | endlist: true 8 | }); 9 | 10 | function createSegments() { 11 | const segments = []; 12 | segments.push(new Segment({ 13 | uri: 'http://media.example.com/first.ts', 14 | duration: 9.009, 15 | title: '', 16 | mediaSequenceNumber: 0, 17 | discontinuitySequence: 0 18 | })); 19 | segments.push(new Segment({ 20 | uri: 'http://media.example.com/second.ts', 21 | duration: 9.009, 22 | title: '', 23 | mediaSequenceNumber: 1, 24 | discontinuitySequence: 0, 25 | markers: [{ 26 | type: 'OUT', 27 | duration: 15 28 | }] 29 | })); 30 | segments.push(new Segment({ 31 | uri: 'http://media.example.com/third.ts', 32 | duration: 3.003, 33 | title: '', 34 | mediaSequenceNumber: 2, 35 | discontinuitySequence: 0 36 | })); 37 | return segments; 38 | } 39 | 40 | module.exports = playlist; 41 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | const TerserPlugin = require('terser-webpack-plugin'); 3 | 4 | module.exports = (_, argv) => { 5 | return { 6 | entry: './index.ts', 7 | output: { 8 | path: path.resolve(__dirname, 'dist'), 9 | filename: `hls-parser${argv.mode === 'production' ? '.min' : ''}.js`, 10 | library: 'HLS', 11 | libraryTarget: 'umd' 12 | }, 13 | module: { 14 | rules: [ 15 | { 16 | test: /\.js$/, 17 | exclude: /node_modules/, 18 | loader: 'babel-loader', 19 | options: { 20 | presets: ['@babel/preset-env'] 21 | } 22 | }, 23 | { 24 | test: /\.ts$/, 25 | exclude: /node_modules/, 26 | use: 'ts-loader' 27 | }, 28 | ] 29 | }, 30 | resolve: { 31 | extensions: ['.js', '.ts'], 32 | }, 33 | devtool: argv.mode === 'production' ? 'source-map' : false, 34 | optimization: { 35 | minimize: argv.mode === 'production', 36 | minimizer: [ 37 | new TerserPlugin() 38 | ] 39 | } 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kuu Miyazaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /test/fixtures/objects/8.2-Live-Media-Playlist_using-HTTPS.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment} = require('../../../types'); 2 | 3 | const mediaSequenceBase = 2680; 4 | 5 | const playlist = new MediaPlaylist({ 6 | version: 3, 7 | targetDuration: 8, 8 | mediaSequenceBase, 9 | segments: createSegments() 10 | }); 11 | 12 | function createSegments() { 13 | const segments = []; 14 | segments.push(new Segment({ 15 | uri: 'https://priv.example.com/fileSequence2680.ts', 16 | duration: 7.975, 17 | title: '', 18 | mediaSequenceNumber: mediaSequenceBase + 0, 19 | discontinuitySequence: 0 20 | })); 21 | segments.push(new Segment({ 22 | uri: 'https://priv.example.com/fileSequence2681.ts', 23 | duration: 7.941, 24 | title: '', 25 | mediaSequenceNumber: mediaSequenceBase + 1, 26 | discontinuitySequence: 0 27 | })); 28 | segments.push(new Segment({ 29 | uri: 'https://priv.example.com/fileSequence2682.ts', 30 | duration: 7.975, 31 | title: '', 32 | mediaSequenceNumber: mediaSequenceBase + 2, 33 | discontinuitySequence: 0 34 | })); 35 | return segments; 36 | } 37 | 38 | module.exports = playlist; 39 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.5_EXT-X-PLAYLIST-TYPE.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | 4 | // #EXT-X-PLAYLIST-TYPE: 5 | test('#EXT-X-PLAYLIST-TYPE_01', t => { 6 | const playlist = HLS.parse(` 7 | #EXTM3U 8 | #EXT-X-TARGETDURATION:10 9 | #EXT-X-PLAYLIST-TYPE:EVENT 10 | #EXTINF:10, 11 | http://example.com/1 12 | #EXTINF:10, 13 | http://example.com/2 14 | `); 15 | t.is(playlist.playlistType, 'EVENT'); 16 | }); 17 | 18 | // #EXT-X-PLAYLIST-TYPE: 19 | test('#EXT-X-PLAYLIST-TYPE_02', t => { 20 | const playlist = HLS.parse(` 21 | #EXTM3U 22 | #EXT-X-TARGETDURATION:10 23 | #EXT-X-PLAYLIST-TYPE:VOD 24 | #EXTINF:10, 25 | http://example.com/1 26 | #EXTINF:10, 27 | http://example.com/2 28 | `); 29 | t.is(playlist.playlistType, 'VOD'); 30 | }); 31 | 32 | // #EXT-X-PLAYLIST-TYPE: 33 | test('#EXT-X-PLAYLIST-TYPE_03', t => { 34 | const playlist = HLS.parse(` 35 | #EXTM3U 36 | #EXT-X-TARGETDURATION:10 37 | #EXTINF:10, 38 | http://example.com/1 39 | #EXTINF:10, 40 | http://example.com/2 41 | `); 42 | t.is(playlist.playlistType, undefined); 43 | }); 44 | -------------------------------------------------------------------------------- /test/fixtures/objects/RedundantSegments.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | version: 4, 5 | targetDuration: 10, 6 | segments: createSegments(), 7 | endlist: true 8 | }); 9 | 10 | function createSegments() { 11 | const segments = []; 12 | segments.push(new Segment({ 13 | uri: 'http://media.example.com/first.ts', 14 | duration: 9.009, 15 | title: '', 16 | mediaSequenceNumber: 0, 17 | discontinuitySequence: 0 18 | })); 19 | segments.push(new Segment({ 20 | uri: 'http://media.example.com/second.ts', 21 | duration: 9.009, 22 | title: '', 23 | mediaSequenceNumber: 1, 24 | discontinuitySequence: 0 25 | })); 26 | segments.push(new Segment({ 27 | uri: 'http://media.example.com/second.ts', 28 | byterange: {offset: 256, length: 128}, 29 | duration: 9.009, 30 | title: '', 31 | mediaSequenceNumber: 2, 32 | discontinuitySequence: 0 33 | })); 34 | segments.push(new Segment({ 35 | uri: 'http://media.example.com/third.ts', 36 | duration: 3.003, 37 | title: '', 38 | mediaSequenceNumber: 3, 39 | discontinuitySequence: 0 40 | })); 41 | return segments; 42 | } 43 | 44 | module.exports = playlist; 45 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.1_Basic-Tags/4.3.1.1_EXTM3U.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // It MUST be the first line of every Media Playlist and 5 | // every Master Playlist. 6 | test('#EXTM3U-01', t => { 7 | // Media Playlist 8 | utils.bothPass(t, ` 9 | #EXTM3U 10 | #EXT-X-TARGETDURATION:10 11 | #EXTINF:10, 12 | http://example.com/1 13 | #EXTINF:10, 14 | http://example.com/2 15 | `); 16 | utils.parseFail(t, ` 17 | #EXT-X-TARGETDURATION:10 18 | #EXTM3U 19 | #EXTINF:10, 20 | http://example.com/1 21 | #EXTINF:10, 22 | http://example.com/2 23 | `); 24 | // Master Playlist 25 | utils.bothPass(t, ` 26 | #EXTM3U 27 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 28 | http://example.com/low.m3u8 29 | #EXT-X-STREAM-INF:BANDWIDTH=2560000 30 | http://example.com/mid.m3u8 31 | #EXT-X-STREAM-INF:BANDWIDTH=7680000 32 | http://example.com/hi.m3u8 33 | `); 34 | utils.parseFail(t, ` 35 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 36 | http://example.com/low.m3u8 37 | #EXT-X-STREAM-INF:BANDWIDTH=2560000 38 | http://example.com/mid.m3u8 39 | #EXT-X-STREAM-INF:BANDWIDTH=7680000 40 | http://example.com/hi.m3u8 41 | `); 42 | }); 43 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.1_EXT-X-TARGETDURATION.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // The EXTINF duration of each Media Segment in the Playlist 5 | // file, when rounded to the nearest integer, MUST be less than or equal 6 | // to the target duration 7 | test('#EXT-X-TARGETDURATION_01', t => { 8 | utils.bothPass(t, ` 9 | #EXTM3U 10 | #EXT-X-VERSION:3 11 | #EXT-X-TARGETDURATION:10 12 | #EXTINF:9.9, 13 | http://example.com/1 14 | #EXTINF:10.4, 15 | http://example.com/2 16 | `); 17 | utils.parseFail(t, ` 18 | #EXTM3U 19 | #EXT-X-VERSION:3 20 | #EXT-X-TARGETDURATION:10 21 | #EXTINF:9.9, 22 | http://example.com/1 23 | #EXTINF:10.5, 24 | http://example.com/2 25 | `); 26 | }); 27 | 28 | // The EXT-X-TARGETDURATION tag is REQUIRED. 29 | test('#EXT-X-TARGETDURATION_02', t => { 30 | utils.parseFail(t, ` 31 | #EXTM3U 32 | #EXTINF:9, 33 | http://example.com/1 34 | #EXTINF:10, 35 | http://example.com/2 36 | `); 37 | utils.bothPass(t, ` 38 | #EXTM3U 39 | #EXT-X-TARGETDURATION:10 40 | #EXTINF:9, 41 | http://example.com/1 42 | #EXTINF:10, 43 | http://example.com/2 44 | `); 45 | }); 46 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.m3u8: -------------------------------------------------------------------------------- 1 | # In this example, the CODECS attributes have been condensed for space. 2 | # A '\' is used to indicate that the tag continues on the following 3 | # line with whitespace removed: 4 | 5 | #EXTM3U 6 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.easy-to-read",URI="main/english-audio.m3u8" 7 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Deutsch",DEFAULT=NO,AUTOSELECT=YES,LANGUAGE="de",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.easy-to-read",URI="main/german-audio.m3u8" 8 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac",NAME="Commentary",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog,public.easy-to-read",URI="commentary/audio-only.m3u8" 9 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="mp4a.40.2",AUDIO="aac" 10 | low/video-only.m3u8 11 | #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="mp4a.40.2",AUDIO="aac" 12 | mid/video-only.m3u8 13 | #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="mp4a.40.2",AUDIO="aac" 14 | hi/video-only.m3u8 15 | #EXT-X-STREAM-INF:BANDWIDTH=65000,CODECS="mp4a.40.5",AUDIO="aac" 16 | main/english-audio.m3u8 17 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.5_EXT-X-SESSION-KEY.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // The value of the METHOD attribute MUST NOT be NONE 5 | test('#EXT-X-SESSION-KEY_01', t => { 6 | utils.parseFail(t, ` 7 | #EXTM3U 8 | #EXT-X-SESSION-KEY:METHOD=NONE 9 | `); 10 | utils.bothPass(t, ` 11 | #EXTM3U 12 | #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com" 13 | `); 14 | }); 15 | 16 | // A Master Playlist MUST NOT contain more than one EXT-X-SESSION-KEY 17 | // tag with the same METHOD, URI, IV, KEYFORMAT, and KEYFORMATVERSIONS 18 | // attribute values. 19 | test('#EXT-X-SESSION-KEY_02', t => { 20 | utils.parseFail(t, ` 21 | #EXTM3U 22 | #EXT-X-VERSION:2 23 | #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 24 | #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 25 | `); 26 | utils.bothPass(t, ` 27 | #EXTM3U 28 | #EXT-X-VERSION:2 29 | #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 30 | #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221101 31 | `); 32 | }); 33 | -------------------------------------------------------------------------------- /test/helpers/matchers.js: -------------------------------------------------------------------------------- 1 | function removeSpaceFromLine(line) { 2 | let inside = false; 3 | let str = ''; 4 | for (const ch of line) { 5 | if (ch === '"') { 6 | inside = !inside; 7 | } else if (!inside && ch === ' ') { 8 | continue; 9 | } 10 | str += ch; 11 | } 12 | return str; 13 | } 14 | 15 | function strip(playlist) { 16 | playlist = playlist.trim(); 17 | const filtered = []; 18 | for (let line of playlist.split('\n')) { 19 | line = removeSpaceFromLine(line); 20 | if (line.startsWith('#')) { 21 | if (line.startsWith('#EXT')) { 22 | filtered.push(line); 23 | } 24 | } else { 25 | filtered.push(line); 26 | } 27 | } 28 | return filtered.join('\n'); 29 | } 30 | 31 | function equalPlaylist(t, expected, actual) { 32 | expected &&= strip(expected); 33 | actual &&= strip(actual); 34 | if (expected === actual) { 35 | return t.pass(); 36 | } 37 | t.fail(`expected="${expected}", actual="${actual}"`); 38 | } 39 | 40 | function notEqualPlaylist(t, expected, actual) { 41 | expected &&= strip(expected); 42 | actual &&= strip(actual); 43 | if (expected === actual) { 44 | t.fail(`expected="${expected}", actual="${actual}"`); 45 | return t.fail(); 46 | } 47 | t.pass(); 48 | } 49 | 50 | module.exports = { 51 | equalPlaylist, 52 | notEqualPlaylist 53 | }; 54 | -------------------------------------------------------------------------------- /test/fixtures/objects/8.5-Master-Playlist-with-I-Frames.js: -------------------------------------------------------------------------------- 1 | const {MasterPlaylist, Variant} = require('../../../types'); 2 | 3 | const playlist = new MasterPlaylist({ 4 | variants: createVariants() 5 | }); 6 | 7 | function createVariants() { 8 | const variants = []; 9 | variants.push(new Variant({ 10 | uri: 'low/audio-video.m3u8', 11 | bandwidth: 1280000, 12 | codecs: 'avc1.640029,mp4a.40.2' 13 | })); 14 | variants.push(new Variant({ 15 | uri: 'low/iframe.m3u8', 16 | isIFrameOnly: true, 17 | bandwidth: 86000, 18 | codecs: 'avc1.640029' 19 | })); 20 | variants.push(new Variant({ 21 | uri: 'mid/audio-video.m3u8', 22 | bandwidth: 2560000, 23 | codecs: 'avc1.640029,mp4a.40.2' 24 | })); 25 | variants.push(new Variant({ 26 | uri: 'mid/iframe.m3u8', 27 | isIFrameOnly: true, 28 | bandwidth: 150000, 29 | codecs: 'avc1.640029' 30 | })); 31 | variants.push(new Variant({ 32 | uri: 'hi/audio-video.m3u8', 33 | bandwidth: 7680000, 34 | codecs: 'avc1.640029,mp4a.40.2' 35 | })); 36 | variants.push(new Variant({ 37 | uri: 'hi/iframe.m3u8', 38 | isIFrameOnly: true, 39 | bandwidth: 550000, 40 | codecs: 'avc1.640029' 41 | })); 42 | variants.push(new Variant({ 43 | uri: 'audio-only.m3u8', 44 | bandwidth: 65000, 45 | codecs: 'mp4a.40.5' 46 | })); 47 | return variants; 48 | } 49 | 50 | module.exports = playlist; 51 | -------------------------------------------------------------------------------- /test/fixtures/objects/SCTE-35_01.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | playlistType: 'VOD', 5 | version: 3, 6 | targetDuration: 8, 7 | segments: createSegments(), 8 | endlist: true 9 | }); 10 | 11 | function createSegments() { 12 | const segments = []; 13 | segments.push(new Segment({ 14 | uri: '1.ts', 15 | duration: 8.008, 16 | title: '', 17 | mediaSequenceNumber: 0, 18 | discontinuitySequence: 0 19 | })); 20 | segments.push(new Segment({ 21 | uri: '2.ts', 22 | duration: 8, 23 | title: '', 24 | mediaSequenceNumber: 1, 25 | discontinuitySequence: 0, 26 | markers: [{ 27 | type: 'OUT', 28 | duration: 15.0 29 | }] 30 | })); 31 | segments.push(new Segment({ 32 | uri: '3.ts', 33 | duration: 7, 34 | title: '', 35 | mediaSequenceNumber: 2, 36 | discontinuitySequence: 0 37 | })); 38 | segments.push(new Segment({ 39 | uri: '4.ts', 40 | duration: 8.008, 41 | title: '', 42 | mediaSequenceNumber: 3, 43 | discontinuitySequence: 0, 44 | markers: [{ 45 | type: 'IN' 46 | }] 47 | })); 48 | segments.push(new Segment({ 49 | uri: '5.ts', 50 | duration: 8.008, 51 | title: '', 52 | mediaSequenceNumber: 4, 53 | discontinuitySequence: 0 54 | })); 55 | return segments; 56 | } 57 | 58 | module.exports = playlist; 59 | -------------------------------------------------------------------------------- /test/fixtures/objects/SCTE-35_03.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | playlistType: 'VOD', 5 | version: 3, 6 | targetDuration: 8, 7 | segments: createSegments(), 8 | endlist: true 9 | }); 10 | 11 | function createSegments() { 12 | const segments = []; 13 | segments.push(new Segment({ 14 | uri: '1.ts', 15 | duration: 8.008, 16 | title: '', 17 | mediaSequenceNumber: 0, 18 | discontinuitySequence: 0 19 | })); 20 | segments.push(new Segment({ 21 | uri: '2.ts', 22 | duration: 8, 23 | title: '', 24 | mediaSequenceNumber: 1, 25 | discontinuitySequence: 0, 26 | markers: [{ 27 | type: 'RAW', 28 | tagName: 'EXT-X-CUE', 29 | value: 'DURATION="15.0",ID="0",TYPE="SpliceOut",TIME="414.171"' 30 | }] 31 | })); 32 | segments.push(new Segment({ 33 | uri: '3.ts', 34 | duration: 7, 35 | title: '', 36 | mediaSequenceNumber: 2, 37 | discontinuitySequence: 0 38 | })); 39 | segments.push(new Segment({ 40 | uri: '4.ts', 41 | duration: 8.008, 42 | title: '', 43 | mediaSequenceNumber: 3, 44 | discontinuitySequence: 0 45 | })); 46 | segments.push(new Segment({ 47 | uri: '5.ts', 48 | duration: 8.008, 49 | title: '', 50 | mediaSequenceNumber: 4, 51 | discontinuitySequence: 0 52 | })); 53 | return segments; 54 | } 55 | 56 | module.exports = playlist; 57 | -------------------------------------------------------------------------------- /test/fixtures/objects/Multiple-rendition-groups.js: -------------------------------------------------------------------------------- 1 | const {MasterPlaylist, Variant, Rendition} = require('../../../types'); 2 | 3 | const renditions = [ 4 | new Rendition({type: 'AUDIO', groupId: 'aac_high', name: 'English', isDefault: true, uri: 'aac_high_eng.m3u8'}), 5 | new Rendition({type: 'AUDIO', groupId: 'aac_high', name: 'Japanese', isDefault: false, uri: 'aac_high_jp.m3u8'}), 6 | new Rendition({type: 'AUDIO', groupId: 'aac_mid', name: 'English', isDefault: true, uri: 'aac_mid_eng.m3u8'}), 7 | new Rendition({type: 'AUDIO', groupId: 'aac_mid', name: 'Japanese', isDefault: false, uri: 'aac_mid_jp.m3u8'}), 8 | new Rendition({type: 'AUDIO', groupId: 'aac_low', name: 'English', isDefault: true, uri: 'aac_low_eng.m3u8'}), 9 | new Rendition({type: 'AUDIO', groupId: 'aac_low', name: 'Japanese', isDefault: false, uri: 'aac_low_jp.m3u8'}), 10 | ]; 11 | const variants = [ 12 | {uri: '1080p.m3u8', bandwidth: 6000000, audioId: 'aac_high'}, 13 | {uri: '720p.m3u8', bandwidth: 3000000, audioId: 'aac_mid'}, 14 | {uri: '540p.m3u8', bandwidth: 1500000, audioId: 'aac_mid'}, 15 | {uri: '360p.m3u8', bandwidth: 1000000, audioId: 'aac_low'}, 16 | ].map( 17 | ({uri, bandwidth, audioId}) => new Variant({ 18 | uri, bandwidth, audio: renditions.filter(({groupId}) => groupId === audioId) 19 | }) 20 | ); 21 | 22 | const playlist = new MasterPlaylist({ 23 | version: 4, 24 | independentSegments: true, 25 | variants, 26 | }); 27 | 28 | module.exports = playlist; 29 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3_Media-Playlist-Tags.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // There MUST NOT be more than one Media Playlist tag of each type in 5 | // any Media Playlist. 6 | test('Media-Playlist-Tags', t => { 7 | utils.bothPass(t, ` 8 | #EXTM3U 9 | #EXT-X-TARGETDURATION:10 10 | #EXTINF:10, 11 | http://example.com/1 12 | #EXTINF:10, 13 | http://example.com/2 14 | `); 15 | utils.parseFail(t, ` 16 | #EXTM3U 17 | #EXT-X-TARGETDURATION:10 18 | #EXTINF:10, 19 | http://example.com/1 20 | #EXT-X-TARGETDURATION:10 21 | #EXTINF:10, 22 | http://example.com/2 23 | `); 24 | }); 25 | 26 | // A Media Playlist Tag MUST NOT appear in a Master Playlist 27 | test('Media-Segment-Tags', t => { 28 | utils.bothPass(t, ` 29 | #EXTM3U 30 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 31 | http://example.com/low.m3u8 32 | #EXT-X-STREAM-INF:BANDWIDTH=2560000 33 | http://example.com/mid.m3u8 34 | #EXT-X-STREAM-INF:BANDWIDTH=7680000 35 | http://example.com/hi.m3u8 36 | `); 37 | utils.parseFail(t, ` 38 | #EXTM3U 39 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 40 | http://example.com/low.m3u8 41 | #EXT-X-STREAM-INF:BANDWIDTH=2560000 42 | http://example.com/mid.m3u8 43 | #EXT-X-STREAM-INF:BANDWIDTH=7680000 44 | http://example.com/hi.m3u8 45 | #EXT-X-ENDLIST 46 | `); 47 | }); 48 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.4.4.7_EXT-X-GAP.spec.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const HLS = require('../../../../..'); 3 | const utils = require("../../../../helpers/utils"); 4 | const {equalPlaylist} = require("../../../../helpers/matchers"); 5 | 6 | // https://datatracker.ietf.org/doc/html/draft-pantos-hls-rfc8216bis#section-4.4.4.7 7 | 8 | test('#EXT-X-TAG_01', t => { 9 | utils.parseFail(t, ` 10 | #EXTM3U 11 | #EXT-X-VERSION:8 12 | #EXT-X-GAP 13 | 1.ts 14 | `); 15 | utils.parsePass(t, ` 16 | #EXTM3U 17 | #EXT-X-VERSION:8 18 | #EXT-X-TARGETDURATION:5 19 | #EXT-X-GAP 20 | #EXTINF:4, 21 | 1.ts 22 | `); 23 | }); 24 | 25 | test('#EXT-X-TAG_02', t => { 26 | utils.parseFail(t, ` 27 | #EXTM3U 28 | #EXT-X-VERSION:8 29 | #EXT-X-TARGETDURATION:5 30 | #EXT-X-GAP 31 | #EXT-X-PART:DURATION=2,URI="1.ts" 32 | #EXT-X-ENDLIST 33 | `); 34 | utils.parsePass(t, ` 35 | #EXTM3U 36 | #EXT-X-VERSION:8 37 | #EXT-X-TARGETDURATION:5 38 | #EXT-X-GAP 39 | #EXT-X-PART:DURATION=2,URI="1.ts",GAP=YES 40 | #EXT-X-ENDLIST 41 | `); 42 | }); 43 | 44 | test('#EXT-X-TAG_03', t => { 45 | const txt = ` 46 | #EXTM3U 47 | #EXT-X-VERSION:8 48 | #EXT-X-TARGETDURATION:5 49 | #EXT-X-GAP 50 | #EXTINF:4, 51 | 1.ts 52 | `; 53 | const playlist = HLS.parse(txt); 54 | t.truthy(playlist.segments[0].gap); 55 | equalPlaylist(t, txt, HLS.stringify(playlist)); 56 | }); 57 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.4_EXT-X-ENDLIST.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // It MAY occur anywhere in the Media Playlist file. 5 | test('#EXT-X-ENDLIST_01', t => { 6 | utils.bothPass(t, ` 7 | #EXTM3U 8 | #EXT-X-ENDLIST 9 | #EXT-X-TARGETDURATION:10 10 | #EXTINF:9, 11 | http://example.com/1 12 | #EXTINF:10, 13 | http://example.com/2 14 | `); 15 | utils.bothPass(t, ` 16 | #EXTM3U 17 | #EXT-X-TARGETDURATION:10 18 | #EXT-X-ENDLIST 19 | #EXTINF:9, 20 | http://example.com/1 21 | #EXTINF:10, 22 | http://example.com/2 23 | `); 24 | utils.bothPass(t, ` 25 | #EXTM3U 26 | #EXT-X-TARGETDURATION:10 27 | #EXTINF:9, 28 | #EXT-X-ENDLIST 29 | http://example.com/1 30 | #EXTINF:10, 31 | http://example.com/2 32 | `); 33 | utils.bothPass(t, ` 34 | #EXTM3U 35 | #EXT-X-TARGETDURATION:10 36 | #EXTINF:9, 37 | http://example.com/1 38 | #EXT-X-ENDLIST 39 | #EXTINF:10, 40 | http://example.com/2 41 | `); 42 | utils.bothPass(t, ` 43 | #EXTM3U 44 | #EXT-X-TARGETDURATION:10 45 | #EXTINF:9, 46 | http://example.com/1 47 | #EXTINF:10, 48 | #EXT-X-ENDLIST 49 | http://example.com/2 50 | `); 51 | utils.bothPass(t, ` 52 | #EXTM3U 53 | #EXT-X-TARGETDURATION:10 54 | #EXTINF:9, 55 | http://example.com/1 56 | #EXTINF:10, 57 | http://example.com/2 58 | #EXT-X-ENDLIST 59 | `); 60 | }); 61 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.2_EXT-X-MEDIA-SEQUENCE.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | const utils = require('../../../../helpers/utils'); 4 | 5 | // If the Media Playlist file does not contain an EXT-X-MEDIA-SEQUENCE 6 | // tag then the Media Sequence Number of the first Media Segment in the 7 | // Media Playlist SHALL be considered to be 0. 8 | test('#EXT-X-MEDIA-SEQUENCE_01', t => { 9 | const playlist = HLS.parse(` 10 | #EXTM3U 11 | #EXT-X-TARGETDURATION:10 12 | #EXTINF:10, 13 | http://example.com/1 14 | #EXTINF:10, 15 | http://example.com/2 16 | `); 17 | t.is(playlist.mediaSequenceBase, 0); 18 | }); 19 | 20 | // The EXT-X-MEDIA-SEQUENCE tag MUST appear before the first Media 21 | // Segment in the Playlist. 22 | test('#EXT-X-MEDIA-SEQUENCE_02', t => { 23 | utils.bothPass(t, ` 24 | #EXTM3U 25 | #EXT-X-TARGETDURATION:10 26 | #EXT-X-MEDIA-SEQUENCE:20 27 | #EXTINF:9, 28 | http://example.com/1 29 | #EXTINF:10, 30 | http://example.com/2 31 | `); 32 | utils.parseFail(t, ` 33 | #EXTM3U 34 | #EXT-X-TARGETDURATION:10 35 | #EXTINF:9, 36 | http://example.com/1 37 | #EXT-X-MEDIA-SEQUENCE:20 38 | #EXTINF:10, 39 | http://example.com/2 40 | `); 41 | utils.bothPass(t, ` 42 | #EXTM3U 43 | #EXT-X-TARGETDURATION:10 44 | #EXTINF:9, 45 | #EXT-X-MEDIA-SEQUENCE:20 46 | http://example.com/1 47 | #EXTINF:10, 48 | http://example.com/2 49 | `); 50 | }); 51 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.1_EXTINF.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // This tag is REQUIRED for each Media Segment 5 | test('#EXTINF_01', t => { 6 | utils.bothPass(t, ` 7 | #EXTM3U 8 | #EXT-X-TARGETDURATION:10 9 | #EXTINF:10, 10 | http://example.com/1 11 | #EXTINF:10, 12 | http://example.com/2 13 | `); 14 | utils.parseFail(t, ` 15 | #EXTM3U 16 | #EXT-X-TARGETDURATION:10 17 | #EXTINF:10, 18 | http://example.com/1 19 | http://example.com/2 20 | `); 21 | }); 22 | 23 | // If the compatibility version number is less than 3, 24 | // durations MUST be integers. 25 | test('#EXTINF_02', t => { 26 | utils.bothPass(t, ` 27 | #EXTM3U 28 | #EXT-X-VERSION:3 29 | #EXT-X-TARGETDURATION:10 30 | #EXTINF:9.9, 31 | http://example.com/1 32 | `); 33 | utils.parseFail(t, ` 34 | #EXTM3U 35 | #EXT-X-VERSION:2 36 | #EXT-X-TARGETDURATION:10 37 | #EXTINF:9.9, 38 | http://example.com/1 39 | `); 40 | }); 41 | 42 | // The remainder of the line following the comma is an optional human- 43 | // readable informative title of the Media Segment expressed as raw 44 | // UTF-8 text. 45 | test('#EXTINF_03', t => { 46 | utils.bothPass(t, ` 47 | #EXTM3U 48 | #EXT-X-TARGETDURATION:10 49 | #EXTINF:10,abc 50 | http://example.com/1 51 | `); 52 | utils.bothPass(t, ` 53 | #EXTM3U 54 | #EXT-X-TARGETDURATION:10 55 | #EXTINF:10,${unescape(encodeURIComponent('\u3042'))} 56 | http://example.com/1 57 | `); 58 | }); 59 | -------------------------------------------------------------------------------- /test/fixtures/objects/8.3-Playlist-with-encrypted-Media-Segments.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment, Key} = require('../../../types'); 2 | 3 | const mediaSequenceBase = 7794; 4 | 5 | const key1 = new Key({method: 'AES-128', uri: 'https://priv.example.com/key.php?r=52'}); 6 | const key2 = new Key({method: 'AES-128', uri: 'https://priv.example.com/key.php?r=53'}); 7 | 8 | const playlist = new MediaPlaylist({ 9 | version: 3, 10 | targetDuration: 15, 11 | mediaSequenceBase, 12 | segments: createSegments() 13 | }); 14 | 15 | function createSegments() { 16 | const segments = []; 17 | segments.push(new Segment({ 18 | uri: 'http://media.example.com/fileSequence52-A.ts', 19 | duration: 2.833, 20 | title: '', 21 | mediaSequenceNumber: mediaSequenceBase + 0, 22 | discontinuitySequence: 0, 23 | key: key1 24 | })); 25 | segments.push(new Segment({ 26 | uri: 'http://media.example.com/fileSequence52-B.ts', 27 | duration: 15.0, 28 | title: '', 29 | mediaSequenceNumber: mediaSequenceBase + 1, 30 | discontinuitySequence: 0, 31 | key: key1 32 | })); 33 | segments.push(new Segment({ 34 | uri: 'http://media.example.com/fileSequence52-C.ts', 35 | duration: 13.333, 36 | title: '', 37 | mediaSequenceNumber: mediaSequenceBase + 2, 38 | discontinuitySequence: 0, 39 | key: key1 40 | })); 41 | segments.push(new Segment({ 42 | uri: 'http://media.example.com/fileSequence53-A.ts', 43 | duration: 15.0, 44 | title: '', 45 | mediaSequenceNumber: mediaSequenceBase + 3, 46 | discontinuitySequence: 0, 47 | key: key2 48 | })); 49 | return segments; 50 | } 51 | 52 | module.exports = playlist; 53 | -------------------------------------------------------------------------------- /test/fixtures/objects/SCTE-35_05.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | playlistType: 'VOD', 5 | version: 3, 6 | targetDuration: 8, 7 | segments: createSegments(), 8 | endlist: true 9 | }); 10 | 11 | function createSegments() { 12 | const segments = []; 13 | segments.push(new Segment({ 14 | uri: '1.ts', 15 | duration: 8.008, 16 | title: '', 17 | mediaSequenceNumber: 0, 18 | discontinuitySequence: 0 19 | })); 20 | segments.push(new Segment({ 21 | uri: '2.ts', 22 | duration: 8, 23 | title: '', 24 | mediaSequenceNumber: 1, 25 | discontinuitySequence: 0, 26 | markers: [{ 27 | type: 'RAW', 28 | tagName: 'EXT-X-SCTE35', 29 | value: 'TYPE=0x34,DURATION=15.0,CUE-OUT=YES,UPID="0x08:0x9425BC",CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”pIViS5”' 30 | }] 31 | })); 32 | segments.push(new Segment({ 33 | uri: '3.ts', 34 | duration: 7, 35 | title: '', 36 | mediaSequenceNumber: 2, 37 | discontinuitySequence: 0 38 | })); 39 | segments.push(new Segment({ 40 | uri: '4.ts', 41 | duration: 8.008, 42 | title: '', 43 | mediaSequenceNumber: 3, 44 | discontinuitySequence: 0, 45 | markers: [{ 46 | type: 'RAW', 47 | tagName: 'EXT-X-SCTE35', 48 | value: 'TYPE=0x35,CUE-IN=YES,CUE=”/DA0AAAA+…AAg+2UBNAAANvrtoQ==”,ID=”f6UrRd”' 49 | }] 50 | })); 51 | segments.push(new Segment({ 52 | uri: '5.ts', 53 | duration: 8.008, 54 | title: '', 55 | mediaSequenceNumber: 4, 56 | discontinuitySequence: 0 57 | })); 58 | return segments; 59 | } 60 | 61 | module.exports = playlist; 62 | -------------------------------------------------------------------------------- /test/fixtures/objects/SCTE-35_02.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | playlistType: 'VOD', 5 | version: 3, 6 | targetDuration: 8, 7 | segments: createSegments(), 8 | endlist: true 9 | }); 10 | 11 | function createSegments() { 12 | const segments = []; 13 | segments.push(new Segment({ 14 | uri: '1.ts', 15 | duration: 8.008, 16 | title: '', 17 | mediaSequenceNumber: 0, 18 | discontinuitySequence: 0 19 | })); 20 | segments.push(new Segment({ 21 | uri: '2.ts', 22 | duration: 8, 23 | title: '', 24 | mediaSequenceNumber: 1, 25 | discontinuitySequence: 0, 26 | markers: [{ 27 | type: 'OUT', 28 | duration: 23.0 29 | }] 30 | })); 31 | segments.push(new Segment({ 32 | uri: '3.ts', 33 | duration: 8, 34 | title: '', 35 | mediaSequenceNumber: 2, 36 | discontinuitySequence: 0, 37 | markers: [{ 38 | type: 'RAW', 39 | tagName: 'EXT-X-CUE-OUT-CONT', 40 | value: 'ElapsedTime=8,Duration=23' 41 | }] 42 | })); 43 | segments.push(new Segment({ 44 | uri: '4.ts', 45 | duration: 7, 46 | title: '', 47 | mediaSequenceNumber: 3, 48 | discontinuitySequence: 0, 49 | markers: [{ 50 | type: 'RAW', 51 | tagName: 'EXT-X-CUE-OUT-CONT', 52 | value: 'ElapsedTime=16,Duration=23' 53 | }] 54 | })); 55 | segments.push(new Segment({ 56 | uri: '5.ts', 57 | duration: 8.008, 58 | title: '', 59 | mediaSequenceNumber: 4, 60 | discontinuitySequence: 0, 61 | markers: [{ 62 | type: 'IN' 63 | }] 64 | })); 65 | return segments; 66 | } 67 | 68 | module.exports = playlist; 69 | -------------------------------------------------------------------------------- /test/fixtures/objects/8.7-Master-Playlist-with-Alternative-video.js: -------------------------------------------------------------------------------- 1 | const {MasterPlaylist, Variant, Rendition} = require('../../../types'); 2 | 3 | function createRendition(groupId) { 4 | const renditions = []; 5 | renditions.push(new Rendition({ 6 | type: 'VIDEO', 7 | uri: `${groupId}/main/audio-video.m3u8`, 8 | groupId, 9 | name: 'Main', 10 | isDefault: !(groupId === 'mid') 11 | })); 12 | renditions.push(new Rendition({ 13 | type: 'VIDEO', 14 | uri: `${groupId}/centerfield/audio-video.m3u8`, 15 | groupId, 16 | name: 'Centerfield', 17 | isDefault: groupId === 'mid' 18 | })); 19 | renditions.push(new Rendition({ 20 | type: 'VIDEO', 21 | uri: `${groupId}/dugout/audio-video.m3u8`, 22 | groupId, 23 | name: 'Dugout', 24 | isDefault: false 25 | })); 26 | return renditions; 27 | } 28 | 29 | const playlist = new MasterPlaylist({ 30 | variants: createVariants() 31 | }); 32 | 33 | function createVariants() { 34 | const variants = []; 35 | variants.push(new Variant({ 36 | uri: 'low/main/audio-video.m3u8', 37 | bandwidth: 1280000, 38 | codecs: 'avc1.640029,mp4a.40.2', 39 | video: createRendition('low'), 40 | currentRenditions: {video: 0} 41 | })); 42 | variants.push(new Variant({ 43 | uri: 'mid/main/audio-video.m3u8', 44 | bandwidth: 2560000, 45 | codecs: 'avc1.640029,mp4a.40.2', 46 | video: createRendition('mid'), 47 | currentRenditions: {video: 1} 48 | })); 49 | variants.push(new Variant({ 50 | uri: 'hi/main/audio-video.m3u8', 51 | bandwidth: 7680000, 52 | codecs: 'avc1.640029,mp4a.40.2', 53 | video: createRendition('hi') 54 | })); 55 | return variants; 56 | } 57 | 58 | module.exports = playlist; 59 | -------------------------------------------------------------------------------- /test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-01.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment, PartialSegment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | version: 9, 5 | targetDuration: 4, 6 | mediaSequenceBase: 266, 7 | lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02}, 8 | partTargetDuration: 1.02, 9 | skip: 3, 10 | segments: createSegments() 11 | }); 12 | 13 | function createSegments() { 14 | const segments = []; 15 | segments.push(new Segment({ 16 | uri: 'fileSequence269.mp4', 17 | duration: 4.00008, 18 | title: '', 19 | mediaSequenceNumber: 269, 20 | discontinuitySequence: 0 21 | })); 22 | segments.push(new Segment({ 23 | uri: 'fileSequence270.mp4', 24 | duration: 4.00008, 25 | title: '', 26 | mediaSequenceNumber: 270, 27 | discontinuitySequence: 0 28 | })); 29 | segments.push(new Segment({ 30 | mediaSequenceNumber: 271, 31 | parts: createParts() 32 | })); 33 | return segments; 34 | } 35 | 36 | function createParts() { 37 | const parts = []; 38 | parts.push(new PartialSegment({ 39 | uri: 'fileSequence271.mp4', 40 | duration: 1.02, 41 | byterange: {offset: 0, length: 20000} 42 | })); 43 | parts.push(new PartialSegment({ 44 | uri: 'fileSequence271.mp4', 45 | duration: 1.02, 46 | byterange: {offset: 20000, length: 23000} 47 | })); 48 | parts.push(new PartialSegment({ 49 | uri: 'fileSequence271.mp4', 50 | duration: 1.02, 51 | byterange: {offset: 43000, length: 18000} 52 | })); 53 | parts.push(new PartialSegment({ 54 | uri: 'fileSequence271.mp4', 55 | byterange: {offset: 61000}, 56 | hint: true 57 | })); 58 | return parts; 59 | } 60 | 61 | module.exports = playlist; 62 | -------------------------------------------------------------------------------- /test/spec/HLSJS-LHLS/02_EXT-X-PREFETCH-DISCONTINUITY.spec.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const utils = require("../../helpers/utils"); 3 | const HLS = require("../../.."); 4 | 5 | test("#EXT-X-PREFETCH-DISCONTINUITY_01", t => { 6 | const text = ` 7 | #EXTM3U 8 | #EXT-X-VERSION:3 9 | #EXT-X-TARGETDURATION:2 10 | #EXT-X-MEDIA-SEQUENCE: 0 11 | #EXT-X-DISCONTINUITY-SEQUENCE: 0 12 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z 13 | #EXTINF:2.000 14 | https://foo.com/bar/0.ts 15 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z 16 | #EXTINF:2.000 17 | https://foo.com/bar/1.ts 18 | 19 | #EXT-X-PREFETCH-DISCONTINUITY 20 | #EXT-X-PREFETCH:https://foo.com/bar/5.ts 21 | #EXT-X-PREFETCH:https://foo.com/bar/6.ts 22 | `; 23 | utils.bothPass(t, text); 24 | const {prefetchSegments} = HLS.parse(text); 25 | t.is(prefetchSegments.length, 2); 26 | t.true(prefetchSegments[0].discontinuity); 27 | t.falsy(prefetchSegments[1].discontinuity); 28 | }); 29 | 30 | test("#EXT-X-PREFETCH-DISCONTINUITY_02", t => { 31 | const text = ` 32 | #EXTM3U 33 | #EXT-X-VERSION:3 34 | #EXT-X-TARGETDURATION:2 35 | #EXT-X-MEDIA-SEQUENCE: 1 36 | #EXT-X-DISCONTINUITY-SEQUENCE: 0 37 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z 38 | #EXTINF:2.000 39 | https://foo.com/bar/1.ts 40 | #EXT-X-DISCONTINUITY 41 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T21:59:10.531Z 42 | #EXTINF:2.000 43 | https://foo.com/bar/5.ts 44 | 45 | #EXT-X-PREFETCH:https://foo.com/bar/6.ts 46 | #EXT-X-PREFETCH-DISCONTINUITY 47 | #EXT-X-PREFETCH:https://foo.com/bar/9.ts 48 | `; 49 | utils.bothPass(t, text); 50 | const {prefetchSegments} = HLS.parse(text); 51 | t.is(prefetchSegments.length, 2); 52 | t.falsy(prefetchSegments[0].discontinuity); 53 | t.true(prefetchSegments[1].discontinuity); 54 | }); 55 | -------------------------------------------------------------------------------- /test/fixtures/objects/SCTE-35_04.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | playlistType: 'VOD', 5 | version: 3, 6 | targetDuration: 8, 7 | segments: createSegments(), 8 | endlist: true 9 | }); 10 | 11 | function createSegments() { 12 | const segments = []; 13 | segments.push(new Segment({ 14 | uri: '1.ts', 15 | duration: 8.008, 16 | title: '', 17 | mediaSequenceNumber: 0, 18 | discontinuitySequence: 0 19 | })); 20 | segments.push(new Segment({ 21 | uri: '2.ts', 22 | duration: 8, 23 | title: '', 24 | mediaSequenceNumber: 1, 25 | discontinuitySequence: 0, 26 | markers: [ 27 | { 28 | type: 'RAW', 29 | tagName: 'EXT-OATCLS-SCTE35', 30 | value: '/DA0AAAAAAAAAAAABQb+ADAQ6QAeAhxDVUVJQAAAO3/PAAEUrEoICAAAAAAg+2UBNAAANvrtoQ==' 31 | }, 32 | { 33 | type: 'RAW', 34 | tagName: 'EXT-X-ASSET', 35 | value: 'CAID=0x0000000020FB6501' 36 | }, 37 | { 38 | type: 'OUT', 39 | duration: 15.0 40 | } 41 | ] 42 | })); 43 | segments.push(new Segment({ 44 | uri: '3.ts', 45 | duration: 7, 46 | title: '', 47 | mediaSequenceNumber: 2, 48 | discontinuitySequence: 0, 49 | markers: [{ 50 | type: 'RAW', 51 | tagName: 'EXT-X-CUE-OUT-CONT', 52 | value: 'ElapsedTime=5.939,Duration=25.0,SCTE35=/DA0AAAA+…AAg+2UBNAAANvrtoQ==' 53 | }] 54 | })); 55 | segments.push(new Segment({ 56 | uri: '4.ts', 57 | duration: 8.008, 58 | title: '', 59 | mediaSequenceNumber: 3, 60 | discontinuitySequence: 0, 61 | markers: [{ 62 | type: 'IN' 63 | }] 64 | })); 65 | segments.push(new Segment({ 66 | uri: '5.ts', 67 | duration: 8.008, 68 | title: '', 69 | mediaSequenceNumber: 4, 70 | discontinuitySequence: 0 71 | })); 72 | return segments; 73 | } 74 | 75 | module.exports = playlist; 76 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.4_EXT-X-SESSION-DATA.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../../helpers/utils'); 3 | 4 | // DATA-ID attribute is REQUIRED. 5 | test('#EXT-X-SESSION-DATA_01', t => { 6 | utils.parseFail(t, ` 7 | #EXTM3U 8 | #EXT-X-SESSION-DATA:LANGUAGE="en",VALUE="This is an example" 9 | `); 10 | utils.bothPass(t, ` 11 | #EXTM3U 12 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" 13 | `); 14 | }); 15 | 16 | // Each EXT-X-SESSION-DATA tag MUST contain either a VALUE or URI 17 | // attribute, but not both. 18 | test('#EXT-X-SESSION-DATA_02', t => { 19 | utils.parseFail(t, ` 20 | #EXTM3U 21 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en" 22 | `); 23 | utils.bothPass(t, ` 24 | #EXTM3U 25 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" 26 | `); 27 | utils.bothPass(t, ` 28 | #EXTM3U 29 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",URI="/data/title.json" 30 | `); 31 | utils.parseFail(t, ` 32 | #EXTM3U 33 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example",URI="/data/title.json" 34 | `); 35 | }); 36 | 37 | // A Playlist MUST NOT contain more than one EXT-X-SESSION-DATA tag 38 | // with the same DATA-ID attribute and the same LANGUAGE attribute. 39 | test('#EXT-X-SESSION-DATA_03', t => { 40 | utils.parseFail(t, ` 41 | #EXTM3U 42 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" 43 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" 44 | `); 45 | utils.bothPass(t, ` 46 | #EXTM3U 47 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="en",VALUE="This is an example" 48 | #EXT-X-SESSION-DATA:DATA-ID="com.example.title",LANGUAGE="ja",VALUE="This is an example" 49 | `); 50 | }); 51 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/8.7-Master-Playlist-with-Alternative-video.m3u8: -------------------------------------------------------------------------------- 1 | # This example shows 3 different video Renditions (Main, Centerfield 2 | # and Dugout), and 3 different Variant Streams (low, mid and high). In 3 | # this example, clients that did not support the EXT-X-MEDIA tag and 4 | # the VIDEO attribute of the EXT-X-STREAM-INF tag would only be able to 5 | # play the video Rendition "Main". 6 | # Since the EXT-X-STREAM-INF tag has no AUDIO attribute, all video 7 | # Renditions would be required to contain the audio. 8 | # 9 | # In this example, the CODECS attributes have been condensed for space. 10 | # A '\' is used to indicate that the tag continues on the following 11 | # line with whitespace removed: 12 | 13 | #EXTM3U 14 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Main",DEFAULT=YES,URI="low/main/audio-video.m3u8" 15 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Centerfield",DEFAULT=NO,URI="low/centerfield/audio-video.m3u8" 16 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="low",NAME="Dugout",DEFAULT=NO,URI="low/dugout/audio-video.m3u8" 17 | 18 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,CODECS="avc1.640029,mp4a.40.2",VIDEO="low" 19 | low/main/audio-video.m3u8 20 | 21 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Main",DEFAULT=NO,URI="mid/main/audio-video.m3u8" 22 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Centerfield",DEFAULT=YES,URI="mid/centerfield/audio-video.m3u8" 23 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="mid",NAME="Dugout",DEFAULT=NO,URI="mid/dugout/audio-video.m3u8" 24 | 25 | #EXT-X-STREAM-INF:BANDWIDTH=2560000,CODECS="avc1.640029,mp4a.40.2",VIDEO="mid" 26 | mid/main/audio-video.m3u8 27 | 28 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Main",DEFAULT=YES,URI="hi/main/audio-video.m3u8" 29 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Centerfield",DEFAULT=NO,URI="hi/centerfield/audio-video.m3u8" 30 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="hi",NAME="Dugout",DEFAULT=NO,URI="hi/dugout/audio-video.m3u8" 31 | 32 | #EXT-X-STREAM-INF:BANDWIDTH=7680000,CODECS="avc1.640029,mp4a.40.2",VIDEO="hi" 33 | hi/main/audio-video.m3u8 34 | -------------------------------------------------------------------------------- /test/fixtures/objects/8.6-Master-Playlist-with-Alternative-audio.js: -------------------------------------------------------------------------------- 1 | const {MasterPlaylist, Variant, Rendition} = require('../../../types'); 2 | 3 | const renditions = createRendition(); 4 | 5 | function createRendition() { 6 | const renditions = []; 7 | renditions.push(new Rendition({ 8 | type: 'AUDIO', 9 | uri: 'main/english-audio.m3u8', 10 | groupId: 'aac', 11 | language: 'en', 12 | name: 'English', 13 | isDefault: true, 14 | autoselect: true 15 | })); 16 | renditions.push(new Rendition({ 17 | type: 'AUDIO', 18 | uri: 'main/german-audio.m3u8', 19 | groupId: 'aac', 20 | language: 'de', 21 | name: 'Deutsch', 22 | isDefault: false, 23 | autoselect: true 24 | })); 25 | renditions.push(new Rendition({ 26 | type: 'AUDIO', 27 | uri: 'commentary/audio-only.m3u8', 28 | groupId: 'aac', 29 | language: 'en', 30 | name: 'Commentary', 31 | isDefault: false, 32 | autoselect: false 33 | })); 34 | return renditions; 35 | } 36 | 37 | const playlist = new MasterPlaylist({ 38 | variants: createVariants() 39 | }); 40 | 41 | function createVariants() { 42 | const variants = []; 43 | variants.push(new Variant({ 44 | uri: 'low/video-only.m3u8', 45 | bandwidth: 1280000, 46 | codecs: 'mp4a.40.2', 47 | audio: renditions, 48 | currentRenditions: {audio: 0} 49 | })); 50 | variants.push(new Variant({ 51 | uri: 'mid/video-only.m3u8', 52 | bandwidth: 2560000, 53 | codecs: 'mp4a.40.2', 54 | audio: renditions, 55 | currentRenditions: {audio: 0} 56 | })); 57 | variants.push(new Variant({ 58 | uri: 'hi/video-only.m3u8', 59 | bandwidth: 7680000, 60 | codecs: 'mp4a.40.2', 61 | audio: renditions, 62 | currentRenditions: {audio: 0} 63 | })); 64 | variants.push(new Variant({ 65 | uri: 'main/english-audio.m3u8', 66 | bandwidth: 65000, 67 | codecs: 'mp4a.40.5', 68 | audio: renditions, 69 | currentRenditions: {audio: 0} 70 | })); 71 | return variants; 72 | } 73 | 74 | module.exports = playlist; 75 | -------------------------------------------------------------------------------- /test/helpers/utils.js: -------------------------------------------------------------------------------- 1 | const HLS = require('../..'); 2 | 3 | HLS.setOptions({strictMode: true}); 4 | 5 | function parsePass(t, text) { 6 | let obj; 7 | try { 8 | obj = HLS.parse(text); 9 | } catch (err) { 10 | t.fail(err.stack); 11 | } 12 | t.truthy(obj); 13 | return obj; 14 | } 15 | 16 | function stringifyPass(t, obj) { 17 | let text; 18 | try { 19 | text = HLS.stringify(obj); 20 | } catch (err) { 21 | t.fail(err.stack); 22 | } 23 | t.truthy(text); 24 | return text; 25 | } 26 | 27 | function bothPass(t, text) { 28 | const obj = parsePass(t, text); 29 | return stringifyPass(t, obj); 30 | } 31 | 32 | function parseFail(t, text) { 33 | try { 34 | HLS.parse(text); 35 | } catch (err) { 36 | return t.truthy(err); 37 | } 38 | t.fail('HLS.parse() did not fail'); 39 | } 40 | 41 | function stringifyFail(t, obj) { 42 | try { 43 | HLS.stringify(obj); 44 | } catch (err) { 45 | return t.truthy(err); 46 | } 47 | t.fail('HLS.stringify() did not fail'); 48 | } 49 | 50 | function stripSpaces(text) { 51 | const chars = []; 52 | let insideDoubleQuotes = false; 53 | for (const ch of text) { 54 | if (ch === '"') { 55 | insideDoubleQuotes = !insideDoubleQuotes; 56 | } else if (ch === ' ') { 57 | if (!insideDoubleQuotes) { 58 | continue; 59 | } 60 | } 61 | chars.push(ch); 62 | } 63 | return chars.join(''); 64 | } 65 | 66 | function stripCommentsAndEmptyLines(text) { 67 | const lines = []; 68 | for (const l of text.split('\n')) { 69 | const line = l.trim(); 70 | if (!line) { 71 | // empty line 72 | continue; 73 | } 74 | if (line.startsWith('#')) { 75 | if (line.startsWith('#EXT')) { 76 | // tag 77 | lines.push(stripSpaces(line)); 78 | } 79 | // comment 80 | continue; 81 | } 82 | // uri 83 | lines.push(line); 84 | } 85 | return lines.join('\n'); 86 | } 87 | 88 | module.exports = { 89 | parsePass, 90 | stringifyPass, 91 | bothPass, 92 | parseFail, 93 | stringifyFail, 94 | stripCommentsAndEmptyLines 95 | }; 96 | -------------------------------------------------------------------------------- /test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-02.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment, PartialSegment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | version: 9, 5 | targetDuration: 4, 6 | mediaSequenceBase: 266, 7 | lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02}, 8 | partTargetDuration: 1.02, 9 | skip: 3, 10 | segments: createSegments() 11 | }); 12 | 13 | function createSegments() { 14 | const segments = []; 15 | segments.push(new Segment({ 16 | uri: 'fileSequence269.mp4', 17 | duration: 4.00008, 18 | title: '', 19 | mediaSequenceNumber: 269, 20 | discontinuitySequence: 0 21 | })); 22 | segments.push(new Segment({ 23 | uri: 'fileSequence270.mp4', 24 | duration: 4.00008, 25 | title: '', 26 | mediaSequenceNumber: 270, 27 | discontinuitySequence: 0 28 | })); 29 | segments.push(new Segment({ 30 | uri: 'fileSequence271.mp4', 31 | duration: 4.00008, 32 | title: '', 33 | mediaSequenceNumber: 271, 34 | discontinuitySequence: 0, 35 | parts: createParts() 36 | })); 37 | segments.push(new Segment({ 38 | mediaSequenceNumber: 272, 39 | parts: [new PartialSegment({ 40 | uri: 'fileSequence272.mp4', 41 | byterange: {offset: 0}, 42 | hint: true 43 | })] 44 | })); 45 | return segments; 46 | } 47 | 48 | function createParts() { 49 | const parts = []; 50 | parts.push(new PartialSegment({ 51 | uri: 'fileSequence271.mp4', 52 | duration: 1.02, 53 | byterange: {offset: 0, length: 20000} 54 | })); 55 | parts.push(new PartialSegment({ 56 | uri: 'fileSequence271.mp4', 57 | duration: 1.02, 58 | byterange: {offset: 20000, length: 23000} 59 | })); 60 | parts.push(new PartialSegment({ 61 | uri: 'fileSequence271.mp4', 62 | duration: 1.02, 63 | byterange: {offset: 43000, length: 18000} 64 | })); 65 | parts.push(new PartialSegment({ 66 | uri: 'fileSequence271.mp4', 67 | duration: 1.02, 68 | byterange: {offset: 61000, length: 19000} 69 | })); 70 | return parts; 71 | } 72 | 73 | module.exports = playlist; 74 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.5_Media-or-Master-Playlist-Tags/4.3.5.1_EXT-X-INDEPENDENT-SEGMENTS.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | const utils = require('../../../../helpers/utils'); 4 | 5 | // The tags in this section can appear in either Master Playlists or 6 | // Media Playlists. 7 | test('#EXT-X-INDEPENDENT-SEGMENTS_01', t => { 8 | const mediaPlaylist = HLS.parse(` 9 | #EXTM3U 10 | #EXT-X-INDEPENDENT-SEGMENTS 11 | #EXT-X-TARGETDURATION:10 12 | #EXTINF:10, 13 | http://example.com/1 14 | #EXTINF:10, 15 | http://example.com/2 16 | `); 17 | t.true(mediaPlaylist.independentSegments); 18 | const masterPlaylist = HLS.parse(` 19 | #EXTM3U 20 | #EXT-X-INDEPENDENT-SEGMENTS 21 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 22 | /video/main.m3u8 23 | #EXT-X-STREAM-INF:BANDWIDTH=640000 24 | /video/low.m3u8 25 | `); 26 | t.true(masterPlaylist.independentSegments); 27 | }); 28 | 29 | // These tags MUST NOT appear more than once in a Playlist. If a tag 30 | // appears more than once, clients MUST reject the playlist. 31 | test('#EXT-X-INDEPENDENT-SEGMENTS_02', t => { 32 | utils.parseFail(t, ` 33 | #EXTM3U 34 | #EXT-X-INDEPENDENT-SEGMENTS 35 | #EXT-X-TARGETDURATION:10 36 | #EXTINF:10, 37 | http://example.com/1 38 | #EXTINF:10, 39 | http://example.com/2 40 | #EXT-X-INDEPENDENT-SEGMENTS 41 | `); 42 | utils.bothPass(t, ` 43 | #EXTM3U 44 | #EXT-X-INDEPENDENT-SEGMENTS 45 | #EXT-X-TARGETDURATION:10 46 | #EXTINF:10, 47 | http://example.com/1 48 | #EXTINF:10, 49 | http://example.com/2 50 | `); 51 | utils.parseFail(t, ` 52 | #EXTM3U 53 | #EXT-X-INDEPENDENT-SEGMENTS 54 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 55 | /video/main.m3u8 56 | #EXT-X-STREAM-INF:BANDWIDTH=640000 57 | /video/low.m3u8 58 | #EXT-X-INDEPENDENT-SEGMENTS 59 | `); 60 | utils.bothPass(t, ` 61 | #EXTM3U 62 | #EXT-X-INDEPENDENT-SEGMENTS 63 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 64 | /video/main.m3u8 65 | #EXT-X-STREAM-INF:BANDWIDTH=640000 66 | /video/low.m3u8 67 | `); 68 | }); 69 | -------------------------------------------------------------------------------- /test/fixtures/objects/SCTE-35_07.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment, DateRange} = require('../../../types'); 2 | const utils = require('../../../utils'); 3 | 4 | const playlist = new MediaPlaylist({ 5 | version: 3, 6 | targetDuration: 30, 7 | segments: createSegments() 8 | }); 9 | 10 | function createSegments() { 11 | const segments = []; 12 | segments.push(new Segment({ 13 | uri: 'http://media.example.com/01.ts', 14 | duration: 30, 15 | title: '', 16 | mediaSequenceNumber: 0, 17 | discontinuitySequence: 0, 18 | programDateTime: new Date('2014-03-05T11:14:00Z') 19 | })); 20 | segments.push(new Segment({ 21 | uri: 'http://media.example.com/02.ts', 22 | duration: 30, 23 | title: '', 24 | mediaSequenceNumber: 1, 25 | discontinuitySequence: 0 26 | })); 27 | segments.push(new Segment({ 28 | uri: 'http://ads.example.com/ad-01.ts', 29 | duration: 30, 30 | title: '', 31 | mediaSequenceNumber: 2, 32 | discontinuitySequence: 0, 33 | dateRange: new DateRange({ 34 | id: 'splice-6FFFFFF0', 35 | start: new Date('2023-10-09T06:16:00.820Z'), 36 | plannedDuration: 59.993, 37 | attributes: { 38 | 'SCTE35-OUT': utils.hexToByteSequence('0xFC30250001D1F7E25300FFF0140565239AA07FEFFE015C3F90FE00526362000101010000A7C1792D') 39 | } 40 | }) 41 | })); 42 | segments.push(new Segment({ 43 | uri: 'http://ads.example.com/ad-02.ts', 44 | duration: 30, 45 | title: '', 46 | mediaSequenceNumber: 3, 47 | discontinuitySequence: 0 48 | })); 49 | segments.push(new Segment({ 50 | uri: 'http://media.example.com/03.ts', 51 | duration: 30, 52 | title: '', 53 | mediaSequenceNumber: 4, 54 | discontinuitySequence: 0, 55 | dateRange: new DateRange({ 56 | id: 'splice-6FFFFFF0', 57 | start: new Date('2023-10-09T06:16:00.820Z'), 58 | end: new Date('2023-10-09T06:17:01.514Z'), 59 | duration: 60.694 60 | }) 61 | })); 62 | segments.push(new Segment({ 63 | uri: 'http://media.example.com/04.ts', 64 | duration: 3.003, 65 | title: '', 66 | mediaSequenceNumber: 5, 67 | discontinuitySequence: 0 68 | })); 69 | return segments; 70 | } 71 | 72 | module.exports = playlist; 73 | -------------------------------------------------------------------------------- /test/fixtures/objects/Low-Latency_Example-03_Byterange-addressed_Parts-03.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment, PartialSegment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | version: 9, 5 | targetDuration: 4, 6 | mediaSequenceBase: 266, 7 | lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.02}, 8 | partTargetDuration: 1.02, 9 | skip: 3, 10 | segments: createSegments() 11 | }); 12 | 13 | function createSegments() { 14 | const segments = []; 15 | segments.push(new Segment({ 16 | uri: 'fileSequence269.mp4', 17 | duration: 4.00008, 18 | title: '', 19 | mediaSequenceNumber: 269, 20 | discontinuitySequence: 0 21 | })); 22 | segments.push(new Segment({ 23 | uri: 'fileSequence270.mp4', 24 | duration: 4.00008, 25 | title: '', 26 | mediaSequenceNumber: 270, 27 | discontinuitySequence: 0 28 | })); 29 | segments.push(new Segment({ 30 | uri: 'fileSequence271.mp4', 31 | duration: 4.00008, 32 | title: '', 33 | mediaSequenceNumber: 271, 34 | discontinuitySequence: 0, 35 | parts: createParts() 36 | })); 37 | segments.push(new Segment({ 38 | mediaSequenceNumber: 272, 39 | parts: [ 40 | new PartialSegment({ 41 | uri: 'fileSequence272.mp4', 42 | duration: 1.02, 43 | byterange: {offset: 0, length: 21000} 44 | }), 45 | new PartialSegment({ 46 | uri: 'fileSequence272.mp4', 47 | byterange: {offset: 21000}, 48 | hint: true 49 | }) 50 | ] 51 | })); 52 | return segments; 53 | } 54 | 55 | function createParts() { 56 | const parts = []; 57 | parts.push(new PartialSegment({ 58 | uri: 'fileSequence271.mp4', 59 | duration: 1.02, 60 | byterange: {offset: 0, length: 20000} 61 | })); 62 | parts.push(new PartialSegment({ 63 | uri: 'fileSequence271.mp4', 64 | duration: 1.02, 65 | byterange: {offset: 20000, length: 23000} 66 | })); 67 | parts.push(new PartialSegment({ 68 | uri: 'fileSequence271.mp4', 69 | duration: 1.02, 70 | byterange: {offset: 43000, length: 18000} 71 | })); 72 | parts.push(new PartialSegment({ 73 | uri: 'fileSequence271.mp4', 74 | duration: 1.02, 75 | byterange: {offset: 61000, length: 19000} 76 | })); 77 | return parts; 78 | } 79 | 80 | module.exports = playlist; 81 | -------------------------------------------------------------------------------- /test/fixtures/objects/8.10-EXT-X-DATERANGE-carrying-SCTE-35-tags.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment, DateRange} = require('../../../types'); 2 | const utils = require('../../../utils'); 3 | 4 | const playlist = new MediaPlaylist({ 5 | version: 3, 6 | targetDuration: 30, 7 | segments: createSegments() 8 | }); 9 | 10 | function createSegments() { 11 | const segments = []; 12 | segments.push(new Segment({ 13 | uri: 'http://media.example.com/01.ts', 14 | duration: 30, 15 | title: '', 16 | mediaSequenceNumber: 0, 17 | discontinuitySequence: 0, 18 | programDateTime: new Date('2014-03-05T11:14:00Z') 19 | })); 20 | segments.push(new Segment({ 21 | uri: 'http://media.example.com/02.ts', 22 | duration: 30, 23 | title: '', 24 | mediaSequenceNumber: 1, 25 | discontinuitySequence: 0 26 | })); 27 | segments.push(new Segment({ 28 | uri: 'http://ads.example.com/ad-01.ts', 29 | duration: 30, 30 | title: '', 31 | mediaSequenceNumber: 2, 32 | discontinuitySequence: 0, 33 | dateRange: new DateRange({ 34 | id: 'splice-6FFFFFF0', 35 | start: new Date('2014-03-05T11:15:00Z'), 36 | plannedDuration: 59.993, 37 | attributes: { 38 | 'SCTE35-OUT': utils.hexToByteSequence('0xFC002F0000000000FF000014056FFFFFF000E011622DCAFF000052636200000000000A0008029896F50000008700000000') 39 | } 40 | }) 41 | })); 42 | segments.push(new Segment({ 43 | uri: 'http://ads.example.com/ad-02.ts', 44 | duration: 30, 45 | title: '', 46 | mediaSequenceNumber: 3, 47 | discontinuitySequence: 0 48 | })); 49 | segments.push(new Segment({ 50 | uri: 'http://media.example.com/03.ts', 51 | duration: 30, 52 | title: '', 53 | mediaSequenceNumber: 4, 54 | discontinuitySequence: 0, 55 | dateRange: new DateRange({ 56 | id: 'splice-6FFFFFF0', 57 | duration: 59.993, 58 | attributes: { 59 | 'SCTE35-IN': utils.hexToByteSequence('0xFC002A0000000000FF00000F056FFFFFF000401162802E6100000000000A0008029896F50000008700000000') 60 | } 61 | }) 62 | })); 63 | segments.push(new Segment({ 64 | uri: 'http://media.example.com/04.ts', 65 | duration: 3.003, 66 | title: '', 67 | mediaSequenceNumber: 5, 68 | discontinuitySequence: 0 69 | })); 70 | return segments; 71 | } 72 | 73 | module.exports = playlist; 74 | -------------------------------------------------------------------------------- /test/fixtures/objects/8.9-CHARACTERISTICS-attribute-containing-multiple-characteristics.js: -------------------------------------------------------------------------------- 1 | const {MasterPlaylist, Variant, Rendition} = require('../../../types'); 2 | 3 | const renditions = createRendition(); 4 | 5 | function createRendition() { 6 | const renditions = []; 7 | renditions.push(new Rendition({ 8 | type: 'AUDIO', 9 | uri: 'main/english-audio.m3u8', 10 | groupId: 'aac', 11 | language: 'en', 12 | name: 'English', 13 | isDefault: true, 14 | autoselect: true, 15 | characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read' 16 | })); 17 | renditions.push(new Rendition({ 18 | type: 'AUDIO', 19 | uri: 'main/german-audio.m3u8', 20 | groupId: 'aac', 21 | language: 'de', 22 | name: 'Deutsch', 23 | isDefault: false, 24 | autoselect: true, 25 | characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read' 26 | })); 27 | renditions.push(new Rendition({ 28 | type: 'AUDIO', 29 | uri: 'commentary/audio-only.m3u8', 30 | groupId: 'aac', 31 | language: 'en', 32 | name: 'Commentary', 33 | isDefault: false, 34 | autoselect: false, 35 | characteristics: 'public.accessibility.transcribes-spoken-dialog,public.easy-to-read' 36 | })); 37 | return renditions; 38 | } 39 | 40 | const playlist = new MasterPlaylist({ 41 | variants: createVariants() 42 | }); 43 | 44 | function createVariants() { 45 | const variants = []; 46 | variants.push(new Variant({ 47 | uri: 'low/video-only.m3u8', 48 | bandwidth: 1280000, 49 | codecs: 'mp4a.40.2', 50 | audio: renditions, 51 | currentRenditions: {audio: 0} 52 | })); 53 | variants.push(new Variant({ 54 | uri: 'mid/video-only.m3u8', 55 | bandwidth: 2560000, 56 | codecs: 'mp4a.40.2', 57 | audio: renditions, 58 | currentRenditions: {audio: 0} 59 | })); 60 | variants.push(new Variant({ 61 | uri: 'hi/video-only.m3u8', 62 | bandwidth: 7680000, 63 | codecs: 'mp4a.40.2', 64 | audio: renditions, 65 | currentRenditions: {audio: 0} 66 | })); 67 | variants.push(new Variant({ 68 | uri: 'main/english-audio.m3u8', 69 | bandwidth: 65000, 70 | codecs: 'mp4a.40.5', 71 | audio: renditions, 72 | currentRenditions: {audio: 0} 73 | })); 74 | return variants; 75 | } 76 | 77 | module.exports = playlist; 78 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.3_Media-Playlist-Tags/4.3.3.3_EXT-X-DISCONTINUITY-SEQUENCE.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | const utils = require('../../../../helpers/utils'); 4 | 5 | // If the Media Playlist does not contain an EXT-X-DISCONTINUITY- 6 | // SEQUENCE tag, then the Discontinuity Sequence Number of the first 7 | // Media Segment in the Playlist SHALL be considered to be 0. 8 | test('#EXT-X-DISCONTINUITY-SEQUENCE_01', t => { 9 | const playlist = HLS.parse(` 10 | #EXTM3U 11 | #EXT-X-TARGETDURATION:10 12 | #EXTINF:10, 13 | http://example.com/1 14 | #EXT-X-DISCONTINUITY 15 | #EXTINF:10, 16 | http://example.com/2 17 | `); 18 | t.is(playlist.discontinuitySequenceBase, 0); 19 | }); 20 | 21 | // The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before the first 22 | // Media Segment in the Playlist. 23 | test('#EXT-X-DISCONTINUITY-SEQUENCE_02', t => { 24 | utils.bothPass(t, ` 25 | #EXTM3U 26 | #EXT-X-TARGETDURATION:10 27 | #EXT-X-DISCONTINUITY-SEQUENCE:20 28 | #EXTINF:9, 29 | http://example.com/1 30 | #EXT-X-DISCONTINUITY 31 | #EXTINF:10, 32 | http://example.com/2 33 | `); 34 | utils.parseFail(t, ` 35 | #EXTM3U 36 | #EXT-X-TARGETDURATION:10 37 | #EXTINF:9, 38 | http://example.com/1 39 | #EXT-X-DISCONTINUITY-SEQUENCE:20 40 | #EXT-X-DISCONTINUITY 41 | #EXTINF:10, 42 | http://example.com/2 43 | `); 44 | utils.bothPass(t, ` 45 | #EXTM3U 46 | #EXT-X-TARGETDURATION:10 47 | #EXTINF:9, 48 | #EXT-X-DISCONTINUITY-SEQUENCE:20 49 | http://example.com/1 50 | #EXT-X-DISCONTINUITY 51 | #EXTINF:10, 52 | http://example.com/2 53 | `); 54 | }); 55 | 56 | // The EXT-X-DISCONTINUITY-SEQUENCE tag MUST appear before any 57 | // EXT-X-DISCONTINUITY tag. 58 | test('#EXT-X-DISCONTINUITY-SEQUENCE_03', t => { 59 | utils.parseFail(t, ` 60 | #EXTM3U 61 | #EXT-X-TARGETDURATION:10 62 | #EXT-X-DISCONTINUITY 63 | #EXT-X-DISCONTINUITY-SEQUENCE:20 64 | #EXTINF:9, 65 | http://example.com/1 66 | #EXTINF:10, 67 | http://example.com/2 68 | `); 69 | utils.bothPass(t, ` 70 | #EXTM3U 71 | #EXT-X-TARGETDURATION:10 72 | #EXT-X-DISCONTINUITY-SEQUENCE:20 73 | #EXT-X-DISCONTINUITY 74 | #EXTINF:9, 75 | http://example.com/1 76 | #EXTINF:10, 77 | http://example.com/2 78 | `); 79 | }); 80 | -------------------------------------------------------------------------------- /test/spec/misc/multiple-rendition-groups.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const utils = require("../../helpers/utils"); 3 | const HLS = require("../../.."); 4 | 5 | test("Multiple-Rendition-Groups_01", t => { 6 | const shouldRead = ` 7 | #EXTM3U 8 | #EXT-X-VERSION:4 9 | #EXT-X-INDEPENDENT-SEGMENTS 10 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="English",DEFAULT=YES,URI="aac_high_eng.m3u8" 11 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="Japanese",DEFAULT=NO,URI="aac_high_jp.m3u8" 12 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="English",DEFAULT=YES,URI="aac_mid_eng.m3u8" 13 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="Japanese",DEFAULT=NO,URI="aac_mid_jp.m3u8" 14 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="English",DEFAULT=YES,URI="aac_low_eng.m3u8" 15 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="Japanese",DEFAULT=NO,URI="aac_low_jp.m3u8" 16 | #EXT-X-STREAM-INF:BANDWIDTH=6000000,AUDIO="aac_high" 17 | 1080p.m3u8 18 | #EXT-X-STREAM-INF:BANDWIDTH=3000000,AUDIO="aac_mid" 19 | 720p.m3u8 20 | #EXT-X-STREAM-INF:BANDWIDTH=1500000,AUDIO="aac_mid" 21 | 540p.m3u8 22 | #EXT-X-STREAM-INF:BANDWIDTH=1000000,AUDIO="aac_low" 23 | 360p.m3u8 24 | `; 25 | 26 | const playlist = HLS.parse(shouldRead); 27 | 28 | const shouldWrite = ` 29 | #EXTM3U 30 | #EXT-X-VERSION:4 31 | #EXT-X-INDEPENDENT-SEGMENTS 32 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="English",DEFAULT=YES,URI="aac_high_eng.m3u8" 33 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_high",NAME="Japanese",DEFAULT=NO,URI="aac_high_jp.m3u8" 34 | #EXT-X-STREAM-INF:BANDWIDTH=6000000,AUDIO="aac_high" 35 | 1080p.m3u8 36 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="English",DEFAULT=YES,URI="aac_mid_eng.m3u8" 37 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_mid",NAME="Japanese",DEFAULT=NO,URI="aac_mid_jp.m3u8" 38 | #EXT-X-STREAM-INF:BANDWIDTH=3000000,AUDIO="aac_mid" 39 | 720p.m3u8 40 | #EXT-X-STREAM-INF:BANDWIDTH=1500000,AUDIO="aac_mid" 41 | 540p.m3u8 42 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="English",DEFAULT=YES,URI="aac_low_eng.m3u8" 43 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aac_low",NAME="Japanese",DEFAULT=NO,URI="aac_low_jp.m3u8" 44 | #EXT-X-STREAM-INF:BANDWIDTH=1000000,AUDIO="aac_low" 45 | 360p.m3u8 46 | `; 47 | 48 | t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(shouldWrite)); 49 | }); 50 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/Low-Latency_Example-02_Playlist_Delta_Update.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | # Following the example above, this Playlist is a response to: GET https://example.com/2M/waitForMSN.php?_HLS_msn=273&_HLS_part=3 &_HLS_skip=YES 3 | #EXT-X-VERSION:9 4 | #EXT-X-TARGETDURATION:4 5 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1 6 | #EXT-X-PART-INF:PART-TARGET=0.33334 7 | #EXT-X-MEDIA-SEQUENCE:266 8 | #EXT-X-SKIP:SKIPPED-SEGMENTS=3 9 | #EXTINF:4.00008, 10 | fileSequence269.mp4 11 | #EXTINF:4.00008, 12 | fileSequence270.mp4 13 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.0.mp4" 14 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.1.mp4" 15 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.2.mp4" 16 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.3.mp4" 17 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.4.mp4",INDEPENDENT=YES 18 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.5.mp4" 19 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.6.mp4" 20 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.7.mp4" 21 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.8.mp4",INDEPENDENT=YES 22 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.9.mp4" 23 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.10.mp4" 24 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.11.mp4" 25 | #EXTINF:4.00008, 26 | fileSequence271.mp4 27 | #EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z 28 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.a.mp4" 29 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.b.mp4" 30 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.c.mp4" 31 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.d.mp4" 32 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.e.mp4" 33 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.f.mp4",INDEPENDENT=YES 34 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.g.mp4" 35 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.h.mp4" 36 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.i.mp4" 37 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.j.mp4" 38 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.k.mp4" 39 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.l.mp4" 40 | #EXTINF:4.00008, 41 | fileSequence272.mp4 42 | #EXT-X-PART:DURATION=0.33334,URI="filePart273.0.mp4",INDEPENDENT=YES 43 | #EXT-X-PART:DURATION=0.33334,URI="filePart273.1.mp4" 44 | #EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4" 45 | #EXT-X-PART:DURATION=0.33334,URI="filePart273.3.mp4" 46 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.4.mp4" 47 | 48 | #EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=3 49 | #EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=3 50 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/Low-Latency_Example-01_Low-Latency_HLS_Playlist.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | # This Playlist is a response to: GET https://example.com/2M/waitForMSN.php?_HLS_msn=273&_HLS_part=2 3 | #EXT-X-VERSION:6 4 | #EXT-X-TARGETDURATION:4 5 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=24,PART-HOLD-BACK=1 6 | #EXT-X-PART-INF:PART-TARGET=0.33334 7 | #EXT-X-MEDIA-SEQUENCE:266 8 | #EXT-X-MAP:URI="init.mp4" 9 | #EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:13:36.106Z 10 | #EXTINF:4.00008, 11 | fileSequence266.mp4 12 | #EXTINF:4.00008, 13 | fileSequence267.mp4 14 | #EXTINF:4.00008, 15 | fileSequence268.mp4 16 | #EXTINF:4.00008, 17 | fileSequence269.mp4 18 | #EXTINF:4.00008, 19 | fileSequence270.mp4 20 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.0.mp4" 21 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.1.mp4" 22 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.2.mp4" 23 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.3.mp4" 24 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.4.mp4",INDEPENDENT=YES 25 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.5.mp4" 26 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.6.mp4" 27 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.7.mp4" 28 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.8.mp4",INDEPENDENT=YES 29 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.9.mp4" 30 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.10.mp4" 31 | #EXT-X-PART:DURATION=0.33334,URI="filePart271.11.mp4" 32 | #EXTINF:4.00008, 33 | fileSequence271.mp4 34 | #EXT-X-PROGRAM-DATE-TIME:2019-02-14T02:14:00.106Z 35 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.a.mp4" 36 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.b.mp4" 37 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.c.mp4" 38 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.d.mp4" 39 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.e.mp4" 40 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.f.mp4",INDEPENDENT=YES 41 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.g.mp4" 42 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.h.mp4" 43 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.i.mp4" 44 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.j.mp4" 45 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.k.mp4" 46 | #EXT-X-PART:DURATION=0.33334,URI="filePart272.l.mp4" 47 | #EXTINF:4.00008, 48 | fileSequence272.mp4 49 | #EXT-X-PART:DURATION=0.33334,URI="filePart273.0.mp4",INDEPENDENT=YES 50 | #EXT-X-PART:DURATION=0.33334,URI="filePart273.1.mp4" 51 | #EXT-X-PART:DURATION=0.33334,URI="filePart273.2.mp4" 52 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="filePart273.3.mp4" 53 | 54 | #EXT-X-RENDITION-REPORT:URI="../1M/waitForMSN.php",LAST-MSN=273,LAST-PART=2 55 | #EXT-X-RENDITION-REPORT:URI="../4M/waitForMSN.php",LAST-MSN=273,LAST-PART=1 56 | -------------------------------------------------------------------------------- /test/fixtures/objects/SCTE-35_06.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | playlistType: 'VOD', 5 | version: 3, 6 | targetDuration: 8, 7 | segments: createSegments(), 8 | endlist: true 9 | }); 10 | 11 | function createSegments() { 12 | const segments = []; 13 | segments.push(new Segment({ 14 | uri: '1.ts', 15 | duration: 8.008, 16 | title: '', 17 | mediaSequenceNumber: 0, 18 | discontinuitySequence: 0 19 | })); 20 | segments.push(new Segment({ 21 | uri: '2.ts', 22 | duration: 8, 23 | title: '', 24 | mediaSequenceNumber: 1, 25 | discontinuitySequence: 0, 26 | markers: [{ 27 | type: 'OUT', 28 | duration: 23.0 29 | }] 30 | })); 31 | segments.push(new Segment({ 32 | uri: '3.ts', 33 | duration: 8, 34 | title: '', 35 | mediaSequenceNumber: 2, 36 | discontinuitySequence: 0, 37 | markers: [{ 38 | type: 'RAW', 39 | tagName: 'EXT-X-CUE-OUT-CONT', 40 | value: 'ElapsedTime=8,Duration=23' 41 | }] 42 | })); 43 | segments.push(new Segment({ 44 | uri: '4.ts', 45 | duration: 7, 46 | title: '', 47 | mediaSequenceNumber: 3, 48 | discontinuitySequence: 0, 49 | markers: [{ 50 | type: 'RAW', 51 | tagName: 'EXT-X-CUE-OUT-CONT', 52 | value: 'ElapsedTime=16,Duration=23' 53 | }] 54 | })); 55 | segments.push(new Segment({ 56 | uri: '5.ts', 57 | duration: 8.008, 58 | title: '', 59 | mediaSequenceNumber: 4, 60 | discontinuitySequence: 0, 61 | markers: [{ 62 | type: 'IN' 63 | }] 64 | })); 65 | segments.push(new Segment({ 66 | uri: '6.ts', 67 | duration: 8, 68 | title: '', 69 | mediaSequenceNumber: 5, 70 | discontinuitySequence: 0, 71 | markers: [{ 72 | type: 'OUT', 73 | duration: 23.0 74 | }] 75 | })); 76 | segments.push(new Segment({ 77 | uri: '7.ts', 78 | duration: 8, 79 | title: '', 80 | mediaSequenceNumber: 6, 81 | discontinuitySequence: 0, 82 | markers: [{ 83 | type: 'RAW', 84 | tagName: 'EXT-X-CUE-OUT-CONT', 85 | value: 'ElapsedTime=8,Duration=23' 86 | }] 87 | })); 88 | segments.push(new Segment({ 89 | uri: '8.ts', 90 | duration: 7, 91 | title: '', 92 | mediaSequenceNumber: 7, 93 | discontinuitySequence: 0, 94 | markers: [{ 95 | type: 'RAW', 96 | tagName: 'EXT-X-CUE-OUT-CONT', 97 | value: 'ElapsedTime=16,Duration=23' 98 | }] 99 | })); 100 | segments.push(new Segment({ 101 | uri: '9.ts', 102 | duration: 8.008, 103 | title: '', 104 | mediaSequenceNumber: 8, 105 | discontinuitySequence: 0, 106 | markers: [{ 107 | type: 'IN' 108 | }] 109 | })); 110 | return segments; 111 | } 112 | 113 | module.exports = playlist; 114 | -------------------------------------------------------------------------------- /test/fixtures/objects/Low-Latency_Example-02_Playlist_Delta_Update.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment, PartialSegment, RenditionReport} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | version: 9, 5 | targetDuration: 4, 6 | mediaSequenceBase: 266, 7 | lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24.0, partHoldBack: 1.0}, 8 | partTargetDuration: 0.33334, 9 | skip: 3, 10 | segments: createSegments(), 11 | renditionReports: createRenditionReports() 12 | }); 13 | 14 | function createSegments() { 15 | const segments = []; 16 | segments.push(new Segment({ 17 | uri: 'fileSequence269.mp4', 18 | duration: 4.00008, 19 | title: '', 20 | mediaSequenceNumber: 269, 21 | discontinuitySequence: 0 22 | })); 23 | segments.push(new Segment({ 24 | uri: 'fileSequence270.mp4', 25 | duration: 4.00008, 26 | title: '', 27 | mediaSequenceNumber: 270, 28 | discontinuitySequence: 0 29 | })); 30 | segments.push(new Segment({ 31 | uri: 'fileSequence271.mp4', 32 | duration: 4.00008, 33 | title: '', 34 | mediaSequenceNumber: 271, 35 | discontinuitySequence: 0, 36 | parts: createParts1() 37 | })); 38 | segments.push(new Segment({ 39 | uri: 'fileSequence272.mp4', 40 | duration: 4.00008, 41 | title: '', 42 | mediaSequenceNumber: 272, 43 | discontinuitySequence: 0, 44 | programDateTime: new Date('2019-02-14T02:14:00.106Z'), 45 | parts: createParts2() 46 | })); 47 | segments.push(new Segment({ 48 | mediaSequenceNumber: 273, 49 | parts: createParts3() 50 | })); 51 | return segments; 52 | } 53 | 54 | function createRenditionReports() { 55 | const reports = []; 56 | reports.push(new RenditionReport({ 57 | uri: '../1M/waitForMSN.php', 58 | lastMSN: 273, 59 | lastPart: 3 60 | })); 61 | reports.push(new RenditionReport({ 62 | uri: '../4M/waitForMSN.php', 63 | lastMSN: 273, 64 | lastPart: 3 65 | })); 66 | return reports; 67 | } 68 | 69 | function createParts1() { 70 | const parts = []; 71 | for (let i = 0; i < 12; i++) { 72 | parts.push(new PartialSegment({ 73 | uri: `filePart271.${i}.mp4`, 74 | duration: 0.33334, 75 | independent: (i === 4 || i === 8) 76 | })); 77 | } 78 | return parts; 79 | } 80 | 81 | function createParts2() { 82 | const parts = []; 83 | const aCode = 'a'.charCodeAt(0); 84 | for (let i = 0; i < 12; i++) { 85 | parts.push(new PartialSegment({ 86 | uri: `filePart272.${String.fromCharCode(aCode + i)}.mp4`, 87 | duration: 0.33334, 88 | independent: (i === 5) 89 | })); 90 | } 91 | return parts; 92 | } 93 | 94 | function createParts3() { 95 | const parts = []; 96 | for (let i = 0; i < 5; i++) { 97 | parts.push(new PartialSegment({ 98 | uri: `filePart273.${i}.mp4`, 99 | duration: 0.33334, 100 | independent: (i === 0), 101 | hint: (i === 4) 102 | })); 103 | } 104 | return parts; 105 | } 106 | 107 | module.exports = playlist; 108 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.5_Media-or-Master-Playlist-Tags/4.3.5.2_EXT-X-START.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | const utils = require('../../../../helpers/utils'); 4 | 5 | // The tags in this section can appear in either Master Playlists or 6 | // Media Playlists. 7 | test('#EXT-X-START_01', t => { 8 | const mediaPlaylist = HLS.parse(` 9 | #EXTM3U 10 | #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES 11 | #EXT-X-TARGETDURATION:10 12 | #EXTINF:10, 13 | http://example.com/1 14 | #EXTINF:10, 15 | http://example.com/2 16 | `); 17 | t.is(mediaPlaylist.start.offset, -10); 18 | t.true(mediaPlaylist.start.precise); 19 | const masterPlaylist = HLS.parse(` 20 | #EXTM3U 21 | #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES 22 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 23 | /video/main.m3u8 24 | #EXT-X-STREAM-INF:BANDWIDTH=640000 25 | /video/low.m3u8 26 | `); 27 | t.is(masterPlaylist.start.offset, -10); 28 | t.true(masterPlaylist.start.precise); 29 | }); 30 | 31 | // These tags MUST NOT appear more than once in a Playlist. If a tag 32 | // appears more than once, clients MUST reject the playlist. 33 | test('#EXT-X-START_02', t => { 34 | utils.parseFail(t, ` 35 | #EXTM3U 36 | #EXT-X-START:TIME-OFFSET=-10 37 | #EXT-X-TARGETDURATION:10 38 | #EXTINF:10, 39 | http://example.com/1 40 | #EXTINF:10, 41 | http://example.com/2 42 | #EXT-X-START:TIME-OFFSET=-20 43 | `); 44 | utils.bothPass(t, ` 45 | #EXTM3U 46 | #EXT-X-START:TIME-OFFSET=-10 47 | #EXT-X-TARGETDURATION:10 48 | #EXTINF:10, 49 | http://example.com/1 50 | #EXTINF:10, 51 | http://example.com/2 52 | `); 53 | utils.parseFail(t, ` 54 | #EXTM3U 55 | #EXT-X-START:TIME-OFFSET=-10 56 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 57 | /video/main.m3u8 58 | #EXT-X-STREAM-INF:BANDWIDTH=640000 59 | /video/low.m3u8 60 | #EXT-X-START:TIME-OFFSET=-20 61 | `); 62 | utils.bothPass(t, ` 63 | #EXTM3U 64 | #EXT-X-START:TIME-OFFSET=-10 65 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 66 | /video/main.m3u8 67 | #EXT-X-STREAM-INF:BANDWIDTH=640000 68 | /video/low.m3u8 69 | `); 70 | }); 71 | 72 | // TIME-OFFSET attribute is REQUIRED. 73 | test('#EXT-X-START_03', t => { 74 | utils.parseFail(t, ` 75 | #EXTM3U 76 | #EXT-X-START:PRECISE=YES 77 | #EXT-X-TARGETDURATION:10 78 | #EXTINF:10, 79 | http://example.com/1 80 | #EXTINF:10, 81 | http://example.com/2 82 | `); 83 | utils.bothPass(t, ` 84 | #EXTM3U 85 | #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES 86 | #EXT-X-TARGETDURATION:10 87 | #EXTINF:10, 88 | http://example.com/1 89 | #EXTINF:10, 90 | http://example.com/2 91 | `); 92 | utils.parseFail(t, ` 93 | #EXTM3U 94 | #EXT-X-START:PRECISE=YES 95 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 96 | /video/main.m3u8 97 | #EXT-X-STREAM-INF:BANDWIDTH=640000 98 | /video/low.m3u8 99 | `); 100 | utils.bothPass(t, ` 101 | #EXTM3U 102 | #EXT-X-START:TIME-OFFSET=-10,PRECISE=YES 103 | #EXT-X-STREAM-INF:BANDWIDTH=1280000 104 | /video/main.m3u8 105 | #EXT-X-STREAM-INF:BANDWIDTH=640000 106 | /video/low.m3u8 107 | `); 108 | }); 109 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/Streaming-Examples_bipbop_16x9_variant.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | 3 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",NAME="BipBop Audio 1",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="eng" 4 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="bipbop_audio",NAME="BipBop Audio 2",DEFAULT=NO,AUTOSELECT=NO,LANGUAGE="eng",URI="alternate_audio_aac/prog_index.m3u8" 5 | 6 | 7 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/eng/prog_index.m3u8" 8 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="English (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="en",URI="subtitles/eng_forced/prog_index.m3u8" 9 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="fr",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/fra/prog_index.m3u8" 10 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Français (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="fr",URI="subtitles/fra_forced/prog_index.m3u8" 11 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="es",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/spa/prog_index.m3u8" 12 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="Español (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="es",URI="subtitles/spa_forced/prog_index.m3u8" 13 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語",DEFAULT=NO,AUTOSELECT=YES,FORCED=NO,LANGUAGE="ja",CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound",URI="subtitles/jpn/prog_index.m3u8" 14 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="subs",NAME="日本語 (Forced)",DEFAULT=NO,AUTOSELECT=NO,FORCED=YES,LANGUAGE="ja",URI="subtitles/jpn_forced/prog_index.m3u8" 15 | 16 | 17 | #EXT-X-STREAM-INF:BANDWIDTH=263851,CODECS="mp4a.40.2, avc1.4d400d",RESOLUTION=416x234,AUDIO="bipbop_audio",SUBTITLES="subs" 18 | gear1/prog_index.m3u8 19 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=28451,URI="gear1/iframe_index.m3u8",CODECS="avc1.4d400d" 20 | 21 | #EXT-X-STREAM-INF:BANDWIDTH=577610,CODECS="mp4a.40.2, avc1.4d401e",RESOLUTION=640x360,AUDIO="bipbop_audio",SUBTITLES="subs" 22 | gear2/prog_index.m3u8 23 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=181534,URI="gear2/iframe_index.m3u8",CODECS="avc1.4d401e" 24 | 25 | #EXT-X-STREAM-INF:BANDWIDTH=915905,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=960x540,AUDIO="bipbop_audio",SUBTITLES="subs" 26 | gear3/prog_index.m3u8 27 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=297056,URI="gear3/iframe_index.m3u8",CODECS="avc1.4d401f" 28 | 29 | #EXT-X-STREAM-INF:BANDWIDTH=1030138,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1280x720,AUDIO="bipbop_audio",SUBTITLES="subs" 30 | gear4/prog_index.m3u8 31 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=339492,URI="gear4/iframe_index.m3u8",CODECS="avc1.4d401f" 32 | 33 | #EXT-X-STREAM-INF:BANDWIDTH=1924009,CODECS="mp4a.40.2, avc1.4d401f",RESOLUTION=1920x1080,AUDIO="bipbop_audio",SUBTITLES="subs" 34 | gear5/prog_index.m3u8 35 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=669554,URI="gear5/iframe_index.m3u8",CODECS="avc1.4d401f" 36 | 37 | #EXT-X-STREAM-INF:BANDWIDTH=41457,CODECS="mp4a.40.2",AUDIO="bipbop_audio",SUBTITLES="subs" 38 | gear0/prog_index.m3u8 39 | -------------------------------------------------------------------------------- /test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/06_EXT-X-SKIP.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../helpers/utils'); 3 | const HLS = require('../../../..'); 4 | 5 | // SKIPPED-SEGMENTS=: (mandatory) 6 | test('#EXT-X-SKIP_01', t => { 7 | utils.bothPass(t, ` 8 | #EXTM3U 9 | #EXT-X-VERSION:9 10 | #EXT-X-TARGETDURATION:2 11 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 12 | #EXT-X-PART-INF:PART-TARGET=0.2 13 | #EXT-X-SKIP:SKIPPED-SEGMENTS=20 14 | #EXTINF:2, 15 | fs240.mp4 16 | #EXTINF:2, 17 | fs241.mp4 18 | #EXTINF:2, 19 | fs242.mp4 20 | #EXTINF:2, 21 | fs243.mp4 22 | #EXTINF:2, 23 | fs244.mp4 24 | #EXTINF:2, 25 | fs245.mp4 26 | `); 27 | utils.parseFail(t, ` 28 | #EXTM3U 29 | #EXT-X-VERSION:9 30 | #EXT-X-TARGETDURATION:2 31 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 32 | #EXT-X-PART-INF:PART-TARGET=0.2 33 | #EXT-X-SKIP:NUM=20 34 | #EXTINF:2, 35 | fs240.mp4 36 | #EXTINF:2, 37 | fs241.mp4 38 | #EXTINF:2, 39 | fs242.mp4 40 | #EXTINF:2, 41 | fs243.mp4 42 | #EXTINF:2, 43 | fs244.mp4 44 | #EXTINF:2, 45 | fs245.mp4 46 | `); 47 | }); 48 | 49 | // SKIPPED-SEGMENTS=: (mandatory) Indicates how many 50 | // Media Segments were replaced by the EXT-X-SKIP tag, 51 | // along with their associated tags. 52 | test('#EXT-X-SKIP_02', t => { 53 | const {skip, segments} = HLS.parse(` 54 | #EXTM3U 55 | #EXT-X-VERSION:9 56 | #EXT-X-TARGETDURATION:2 57 | #EXT-X-MEDIA-SEQUENCE:9000 58 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 59 | #EXT-X-PART-INF:PART-TARGET=0.2 60 | #EXT-X-SKIP:SKIPPED-SEGMENTS=20 61 | #EXTINF:2, 62 | fs240.mp4 63 | #EXTINF:2, 64 | fs241.mp4 65 | #EXTINF:2, 66 | fs242.mp4 67 | #EXTINF:2, 68 | fs243.mp4 69 | #EXTINF:2, 70 | fs244.mp4 71 | #EXTINF:2, 72 | fs245.mp4 73 | `); 74 | 75 | t.is(skip, 20); 76 | t.is(segments[0].mediaSequenceNumber, 9020); 77 | }); 78 | 79 | // A Playlist containing an EXT-X-SKIP tag must have 80 | // an EXT-X-VERSION tag with a value of nine or higher. 81 | test('#EXT-X-SKIP_03', t => { 82 | utils.bothPass(t, ` 83 | #EXTM3U 84 | #EXT-X-VERSION:9 85 | #EXT-X-TARGETDURATION:2 86 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 87 | #EXT-X-PART-INF:PART-TARGET=0.2 88 | #EXT-X-SKIP:SKIPPED-SEGMENTS=20 89 | #EXTINF:2, 90 | fs240.mp4 91 | #EXTINF:2, 92 | fs241.mp4 93 | #EXTINF:2, 94 | fs242.mp4 95 | #EXTINF:2, 96 | fs243.mp4 97 | #EXTINF:2, 98 | fs244.mp4 99 | #EXTINF:2, 100 | fs245.mp4 101 | `); 102 | utils.parseFail(t, ` 103 | #EXTM3U 104 | #EXT-X-VERSION:8 105 | #EXT-X-TARGETDURATION:2 106 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 107 | #EXT-X-PART-INF:PART-TARGET=0.2 108 | #EXT-X-SKIP:SKIPPED-SEGMENTS=20 109 | #EXTINF:2, 110 | fs240.mp4 111 | #EXTINF:2, 112 | fs241.mp4 113 | #EXTINF:2, 114 | fs242.mp4 115 | #EXTINF:2, 116 | fs243.mp4 117 | #EXTINF:2, 118 | fs244.mp4 119 | #EXTINF:2, 120 | fs245.mp4 121 | `); 122 | }); 123 | -------------------------------------------------------------------------------- /test/fixtures/objects/Low-Latency_Example-01_Low-Latency_HLS_Playlist.js: -------------------------------------------------------------------------------- 1 | const {MediaPlaylist, Segment, PartialSegment, MediaInitializationSection, RenditionReport} = require('../../../types'); 2 | 3 | const playlist = new MediaPlaylist({ 4 | version: 6, 5 | targetDuration: 4, 6 | mediaSequenceBase: 266, 7 | lowLatencyCompatibility: {canBlockReload: true, canSkipUntil: 24, partHoldBack: 1}, 8 | partTargetDuration: 0.33334, 9 | segments: createSegments(), 10 | renditionReports: createRenditionReports() 11 | }); 12 | 13 | function createSegments() { 14 | const segments = []; 15 | segments.push(new Segment({ 16 | uri: 'fileSequence266.mp4', 17 | duration: 4.00008, 18 | title: '', 19 | mediaSequenceNumber: 266, 20 | discontinuitySequence: 0, 21 | programDateTime: new Date('2019-02-14T02:13:36.106Z'), 22 | map: new MediaInitializationSection({uri: 'init.mp4'}) 23 | })); 24 | segments.push(new Segment({ 25 | uri: 'fileSequence267.mp4', 26 | duration: 4.00008, 27 | title: '', 28 | mediaSequenceNumber: 267, 29 | discontinuitySequence: 0 30 | })); 31 | segments.push(new Segment({ 32 | uri: 'fileSequence268.mp4', 33 | duration: 4.00008, 34 | title: '', 35 | mediaSequenceNumber: 268, 36 | discontinuitySequence: 0 37 | })); 38 | segments.push(new Segment({ 39 | uri: 'fileSequence269.mp4', 40 | duration: 4.00008, 41 | title: '', 42 | mediaSequenceNumber: 269, 43 | discontinuitySequence: 0 44 | })); 45 | segments.push(new Segment({ 46 | uri: 'fileSequence270.mp4', 47 | duration: 4.00008, 48 | title: '', 49 | mediaSequenceNumber: 270, 50 | discontinuitySequence: 0 51 | })); 52 | segments.push(new Segment({ 53 | uri: 'fileSequence271.mp4', 54 | duration: 4.00008, 55 | title: '', 56 | mediaSequenceNumber: 271, 57 | discontinuitySequence: 0, 58 | parts: createParts1() 59 | })); 60 | segments.push(new Segment({ 61 | uri: 'fileSequence272.mp4', 62 | duration: 4.00008, 63 | title: '', 64 | mediaSequenceNumber: 272, 65 | discontinuitySequence: 0, 66 | programDateTime: new Date('2019-02-14T02:14:00.106Z'), 67 | parts: createParts2() 68 | })); 69 | segments.push(new Segment({ 70 | mediaSequenceNumber: 273, 71 | parts: createParts3() 72 | })); 73 | return segments; 74 | } 75 | 76 | function createRenditionReports() { 77 | const reports = []; 78 | reports.push(new RenditionReport({ 79 | uri: '../1M/waitForMSN.php', 80 | lastMSN: 273, 81 | lastPart: 2 82 | })); 83 | reports.push(new RenditionReport({ 84 | uri: '../4M/waitForMSN.php', 85 | lastMSN: 273, 86 | lastPart: 1 87 | })); 88 | return reports; 89 | } 90 | 91 | function createParts1() { 92 | const parts = []; 93 | for (let i = 0; i < 12; i++) { 94 | parts.push(new PartialSegment({ 95 | uri: `filePart271.${i}.mp4`, 96 | duration: 0.33334, 97 | independent: (i === 4 || i === 8) 98 | })); 99 | } 100 | return parts; 101 | } 102 | 103 | function createParts2() { 104 | const parts = []; 105 | const aCode = 'a'.charCodeAt(0); 106 | for (let i = 0; i < 12; i++) { 107 | parts.push(new PartialSegment({ 108 | uri: `filePart272.${String.fromCharCode(aCode + i)}.mp4`, 109 | duration: 0.33334, 110 | independent: (i === 5) 111 | })); 112 | } 113 | return parts; 114 | } 115 | 116 | function createParts3() { 117 | const parts = []; 118 | for (let i = 0; i < 4; i++) { 119 | parts.push(new PartialSegment({ 120 | uri: `filePart273.${i}.mp4`, 121 | duration: 0.33334, 122 | independent: (i === 0), 123 | hint: (i === 3) 124 | })); 125 | } 126 | return parts; 127 | } 128 | 129 | module.exports = playlist; 130 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.2_EXT-X-BYTERANGE.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | const utils = require('../../../../helpers/utils'); 4 | 5 | // It applies only to the next URI line that follows it in the Playlist. 6 | test('#EXT-X-BYTERANGE_01', t => { 7 | const playlist = HLS.parse(` 8 | #EXTM3U 9 | #EXT-X-VERSION:4 10 | #EXT-X-TARGETDURATION:10 11 | #EXT-X-BYTERANGE:100@200 12 | #EXTINF:10, 13 | http://example.com/1 14 | #EXTINF:10, 15 | http://example.com/2 16 | `); 17 | t.is(playlist.segments[0].byterange.offset, 200); 18 | t.is(playlist.segments[0].byterange.length, 100); 19 | t.falsy(playlist.segments[1].byterange); 20 | }); 21 | 22 | // If o is not present, the sub-range begins at the next byte following 23 | // the sub-range of the previous Media Segment. 24 | test('#EXT-X-BYTERANGE_02', t => { 25 | const playlist = HLS.parse(` 26 | #EXTM3U 27 | #EXT-X-VERSION:4 28 | #EXT-X-TARGETDURATION:10 29 | #EXT-X-BYTERANGE:100@200 30 | #EXTINF:9.9, 31 | http://example.com/1 32 | #EXT-X-BYTERANGE:100 33 | #EXTINF:9.9, 34 | http://example.com/1 35 | #EXT-X-BYTERANGE:100 36 | #EXTINF:9.9, 37 | http://example.com/1 38 | `); 39 | t.is(playlist.segments[0].byterange.offset, 200); 40 | t.is(playlist.segments[1].byterange.offset, 300); 41 | t.is(playlist.segments[2].byterange.offset, 400); 42 | }); 43 | 44 | // If o is not present, a previous Media Segment MUST appear in the 45 | // Playlist file and MUST be a sub-range of the same media resource, or 46 | // the Media Segment is undefined and the Playlist MUST be rejected. 47 | test('#EXT-X-BYTERANGE_03', t => { 48 | utils.parseFail(t, ` 49 | #EXTM3U 50 | #EXT-X-VERSION:4 51 | #EXT-X-TARGETDURATION:10 52 | #EXT-X-BYTERANGE:100 53 | #EXTINF:9.9, 54 | http://example.com/1 55 | `); 56 | utils.parseFail(t, ` 57 | #EXTM3U 58 | #EXT-X-VERSION:4 59 | #EXT-X-TARGETDURATION:10 60 | #EXT-X-BYTERANGE:100@200 61 | #EXTINF:9.9, 62 | http://example.com/1 63 | #EXT-X-BYTERANGE:100 64 | #EXTINF:9.9, 65 | http://example.com/1 66 | #EXT-X-BYTERANGE:100 67 | #EXTINF:9.9, 68 | http://example.com/2 69 | `); 70 | utils.parsePass(t, ` 71 | #EXTM3U 72 | #EXT-X-VERSION:4 73 | #EXT-X-TARGETDURATION:10 74 | #EXT-X-BYTERANGE:100@200 75 | #EXTINF:9.9, 76 | http://example.com/1 77 | #EXT-X-BYTERANGE:100 78 | #EXTINF:9.9, 79 | http://example.com/1 80 | #EXT-X-BYTERANGE:100@200 81 | #EXTINF:9.9, 82 | http://example.com/2 83 | `); 84 | }); 85 | 86 | // Use of the EXT-X-BYTERANGE tag REQUIRES a compatibility version 87 | // number of 4 or greater. 88 | test('#EXT-X-BYTERANGE_04', t => { 89 | utils.parseFail(t, ` 90 | #EXTM3U 91 | #EXT-X-VERSION:3 92 | #EXT-X-TARGETDURATION:10 93 | #EXT-X-BYTERANGE:100@200 94 | #EXTINF:9.9, 95 | http://example.com/1 96 | `); 97 | utils.bothPass(t, ` 98 | #EXTM3U 99 | #EXT-X-VERSION:4 100 | #EXT-X-TARGETDURATION:10 101 | #EXT-X-BYTERANGE:100@200 102 | #EXTINF:9.9, 103 | http://example.com/1 104 | `); 105 | }); 106 | 107 | // EXT-X-BYTERANGE should come at end of segment. 108 | test('#EXT-X-BYTERANGE_05', t => { 109 | t.is( 110 | utils.bothPass(t, ` 111 | #EXTM3U 112 | #EXT-X-VERSION:4 113 | #EXT-X-TARGETDURATION:10 114 | #EXTINF:9.9,comment 115 | #EXT-X-BYTERANGE:100@200 116 | http://example.com/1 117 | #EXT-X-DISCONTINUITY 118 | #EXTINF:9.9,comment 119 | #EXT-X-BYTERANGE:100@200 120 | http://example.com/2 121 | `), 122 | utils.stripCommentsAndEmptyLines(` 123 | #EXTM3U 124 | #EXT-X-VERSION:4 125 | #EXT-X-TARGETDURATION:10 126 | #EXTINF:9.9,comment 127 | #EXT-X-BYTERANGE:100@200 128 | http://example.com/1 129 | #EXT-X-DISCONTINUITY 130 | #EXTINF:9.9,comment 131 | #EXT-X-BYTERANGE:100@200 132 | http://example.com/2 133 | `) 134 | ); 135 | }); 136 | -------------------------------------------------------------------------------- /test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/05_EXT-X-RENDITION-REPORT.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../helpers/utils'); 3 | const HLS = require('../../../..'); 4 | 5 | // URI=: (mandatory) 6 | test('#EXT-X-RENDITION-REPORT_01', t => { 7 | utils.bothPass(t, ` 8 | #EXTM3U 9 | #EXT-X-TARGETDURATION:2 10 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 11 | #EXTINF:2, 12 | fs240.mp4 13 | #EXT-X-RENDITION-REPORT:URI="mid.m3u8",LAST-MSN=1999 14 | #EXT-X-RENDITION-REPORT:URI="high.m3u8",LAST-MSN=1999 15 | `); 16 | utils.parseFail(t, ` 17 | #EXTM3U 18 | #EXT-X-TARGETDURATION:2 19 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 20 | #EXTINF:2, 21 | fs240.mp4 22 | #EXT-X-RENDITION-REPORT:LAST-MSN=1999 23 | #EXT-X-RENDITION-REPORT:LAST-MSN=1999 24 | `); 25 | }); 26 | 27 | // URI=: (mandatory) ... It must be relative to the URI of the Media Playlist 28 | // containing the EXT-X-RENDITION-REPORT tag. 29 | test('#EXT-X-RENDITION-REPORT_02', t => { 30 | utils.bothPass(t, ` 31 | #EXTM3U 32 | #EXT-X-TARGETDURATION:2 33 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 34 | #EXTINF:2, 35 | fs240.mp4 36 | #EXT-X-RENDITION-REPORT:URI="mid.m3u8",LAST-MSN=1999 37 | #EXT-X-RENDITION-REPORT:URI="high.m3u8",LAST-MSN=1999 38 | `); 39 | utils.parseFail(t, ` 40 | #EXTM3U 41 | #EXT-X-TARGETDURATION:2 42 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 43 | #EXTINF:2, 44 | fs240.mp4 45 | #EXT-X-RENDITION-REPORT:URI="https://example.com/mid.m3u8",LAST-MSN=1999 46 | #EXT-X-RENDITION-REPORT:URI="https://example.com/high.m3u8",LAST-MSN=1999 47 | `); 48 | }); 49 | 50 | // A server may omit adding an attribute to an EXT-X-RENDITION-REPORT 51 | // tag — even a mandatory attribute — if its value is the same as that 52 | // of the Rendition Report of the Media Playlist to which the EXT-X-RENDITION-REPORT 53 | // tag is being added. This step reduces the size of the Rendition Report. 54 | test('#EXT-X-RENDITION-REPORT_03', t => { 55 | const {renditionReports} = HLS.parse(` 56 | #EXTM3U 57 | #EXT-X-TARGETDURATION:2 58 | #EXT-X-MEDIA-SEQUENCE:1990 59 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 60 | #EXT-X-PART-INF:PART-TARGET=0.2 61 | #EXTINF:2, 62 | fs240.mp4 63 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 64 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 65 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 66 | #EXT-X-RENDITION-REPORT:URI="main-0.m3u8" 67 | #EXT-X-RENDITION-REPORT:URI="main-1.m3u8" 68 | `); 69 | 70 | t.is(renditionReports.length, 2); 71 | for (const [index, report] of renditionReports.entries()) { 72 | t.is(report.uri, `main-${index}.m3u8`); 73 | t.is(report.lastMSN, 1991); 74 | t.is(report.lastPart, 2); 75 | } 76 | }); 77 | 78 | // Handle 0-indexed segment parts in rendition reports 79 | test('#EXT-X-RENDITION-REPORT_04', t => { 80 | const {renditionReports} = HLS.parse(` 81 | #EXTM3U 82 | #EXT-X-VERSION:6 83 | #EXT-X-TARGETDURATION:3 84 | #EXT-X-SERVER-CONTROL:PART-HOLD-BACK=3.150000,CAN-BLOCK-RELOAD=YES 85 | #EXT-X-PART-INF:PART-TARGET=1 86 | #EXT-X-PROGRAM-DATE-TIME:2022-08-12T15:53:22Z 87 | media_b128000_cmaf_a_6.mp4 88 | #EXT-X-PROGRAM-DATE-TIME:2022-08-12T15:53:31Z 89 | #EXT-X-PART:DURATION=1,INDEPENDENT=YES,URI="media_b128000_cmaf_a_7_p0.mp4" 90 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="media_b128000_cmaf_a_7_p1.mp4" 91 | #EXT-X-RENDITION-REPORT:URI="chunklist_b56000_cmaf_a.m3u8?max_segments=10",LAST-MSN=7,LAST-PART=0 92 | #EXT-X-RENDITION-REPORT:URI="chunklist_b256000_cmaf_a.m3u8?max_segments=10",LAST-MSN=7,LAST-PART=0 93 | `); 94 | 95 | t.is(renditionReports.length, 2); 96 | for (const [index, report] of renditionReports.entries()) { 97 | console.log(index, report); 98 | t.is(report.lastMSN, 7); 99 | t.is(report.lastPart, 0); 100 | } 101 | }); 102 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hls-parser", 3 | "version": "0.16.0", 4 | "description": "A simple library to read/write HLS playlists", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "browser": "dist/hls-parser.min.js", 8 | "scripts": { 9 | "lint": "xo", 10 | "type-check": "tsc --noEmit", 11 | "audit": "npm audit --audit-level high", 12 | "build": "rm -fR ./dist; tsc ; webpack --mode development ; webpack --mode production", 13 | "test": "npm run lint && npm run build && npm run audit && ava --verbose", 14 | "test-offline": "npm run lint && npm run build && ava --verbose" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/kuu/hls-parser.git" 19 | }, 20 | "keywords": [ 21 | "HLS", 22 | "media", 23 | "video", 24 | "audio", 25 | "streaming" 26 | ], 27 | "author": "Kuu Miyazaki", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/kuu/hls-parser/issues" 31 | }, 32 | "homepage": "https://github.com/kuu/hls-parser#readme", 33 | "devDependencies": { 34 | "@ava/typescript": "^6.0.0", 35 | "@babel/core": "^7.28.4", 36 | "@babel/eslint-parser": "^7.28.4", 37 | "@babel/preset-env": "^7.28.3", 38 | "@tsconfig/node18": "^18.2.4", 39 | "ava": "^6.4.1", 40 | "babel-loader": "^10.0.0", 41 | "eslint-plugin-unicorn": "^61.0.2", 42 | "rewire": "^9.0.1", 43 | "terser-webpack-plugin": "^5.3.14", 44 | "ts-loader": "^9.5.4", 45 | "typescript": "^5.9.3", 46 | "webpack": "^5.102.1", 47 | "webpack-cli": "^6.0.1", 48 | "xo": "^0.60.0" 49 | }, 50 | "ava": { 51 | "typescript": { 52 | "compile": "tsc", 53 | "extensions": [ 54 | "ts", 55 | "js" 56 | ], 57 | "rewritePaths": {} 58 | } 59 | }, 60 | "xo": { 61 | "esnext": true, 62 | "space": true, 63 | "rules": { 64 | "arrow-body-style": 0, 65 | "ava/no-ignored-test-files": 0, 66 | "camelcase": 0, 67 | "comma-dangle": 0, 68 | "capitalized-comments": 0, 69 | "dot-notation": 0, 70 | "guard-for-in": 0, 71 | "import/extensions": 0, 72 | "import/no-dynamic-require": 0, 73 | "new-cap": 0, 74 | "no-bitwise": 0, 75 | "no-cond-assign": 0, 76 | "no-mixed-operators": 0, 77 | "no-multi-assign": 0, 78 | "no-use-extend-native/no-use-extend-native": 0, 79 | "object-curly-newline": 0, 80 | "operator-linebreak": 0, 81 | "padding-line-between-statements": 0, 82 | "quotes": 0, 83 | "unicorn/catch-error-name": 0, 84 | "unicorn/filename-case": 0, 85 | "unicorn/no-lonely-if": 0, 86 | "unicorn/no-useless-spread": 0, 87 | "unicorn/no-zero-fractions": 0, 88 | "unicorn/numeric-separators-style": 0, 89 | "unicorn/prefer-code-point": 0, 90 | "unicorn/prefer-module": 0, 91 | "unicorn/prefer-switch": 0, 92 | "unicorn/prevent-abbreviations": 0, 93 | "unicorn/switch-case-braces": 0 94 | }, 95 | "overrides": [ 96 | { 97 | "files": "test/**/*.js", 98 | "rules": { 99 | "unicorn/no-array-push-push": 0 100 | } 101 | }, 102 | { 103 | "files": "*.ts", 104 | "rules": { 105 | "n/file-extension-in-import": 0, 106 | "@typescript-eslint/array-type": 1, 107 | "@typescript-eslint/ban-types": 1, 108 | "@typescript-eslint/comma-dangle": 0, 109 | "@typescript-eslint/consistent-type-imports": 0, 110 | "@typescript-eslint/dot-notation": 0, 111 | "@typescript-eslint/member-delimiter-style": 0, 112 | "@typescript-eslint/naming-convention": 0, 113 | "@typescript-eslint/no-unsafe-call": 0, 114 | "@typescript-eslint/no-unsafe-argument": 0, 115 | "@typescript-eslint/no-unsafe-assignment": 0, 116 | "@typescript-eslint/no-unsafe-return": 0, 117 | "@typescript-eslint/object-curly-spacing": 0, 118 | "@typescript-eslint/padding-line-between-statements": 0, 119 | "@typescript-eslint/prefer-optional-chain": 1, 120 | "@typescript-eslint/prefer-nullish-coalescing": 0, 121 | "@typescript-eslint/quotes": 0, 122 | "@typescript-eslint/restrict-template-expressions": 0, 123 | "@typescript-eslint/restrict-plus-operands": 0, 124 | "unicorn/prefer-export-from": 0 125 | } 126 | } 127 | ], 128 | "settings": { 129 | "import/resolver": { 130 | "node": {} 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /test/spec/misc/scte-35.spec.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const utils = require("../../helpers/utils"); 3 | const HLS = require("../../.."); 4 | 5 | test("#EXT-X-CUE-IN_01", t => { 6 | const {MediaPlaylist, Segment} = HLS.types; 7 | 8 | const segments = [...Array.from({length: 3})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10})); 9 | segments[0].discontinuity = true; 10 | segments[0].markers.push({type: 'OUT', duration: 30}); 11 | 12 | const playlist = new MediaPlaylist({ 13 | targetDuration: 10, 14 | segments 15 | }); 16 | 17 | // For live media playlist, unclosed CUE-OUT is allowed. 18 | const expected = ` 19 | #EXTM3U 20 | #EXT-X-TARGETDURATION:10 21 | #EXT-X-DISCONTINUITY 22 | #EXT-X-CUE-OUT:DURATION=30 23 | #EXTINF:10, 24 | https://example.com/0.ts 25 | #EXTINF:10, 26 | https://example.com/1.ts 27 | #EXTINF:10, 28 | https://example.com/2.ts 29 | `; 30 | 31 | t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected)); 32 | }); 33 | 34 | test("#EXT-X-CUE-IN_02", t => { 35 | const {MediaPlaylist, Segment} = HLS.types; 36 | 37 | const segments = [...Array.from({length: 3})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10})); 38 | segments[0].discontinuity = true; 39 | segments[0].markers.push({type: 'OUT', duration: 30}); 40 | 41 | const playlist = new MediaPlaylist({ 42 | playlistType: 'VOD', 43 | targetDuration: 10, 44 | segments 45 | }); 46 | 47 | // For VOD media playlist, unclosed CUE-OUT is not allowed. 48 | // CUE-IN will be added. 49 | const expected = ` 50 | #EXTM3U 51 | #EXT-X-TARGETDURATION:10 52 | #EXT-X-PLAYLIST-TYPE:VOD 53 | #EXT-X-DISCONTINUITY 54 | #EXT-X-CUE-OUT:DURATION=30 55 | #EXTINF:10, 56 | https://example.com/0.ts 57 | #EXTINF:10, 58 | https://example.com/1.ts 59 | #EXTINF:10, 60 | https://example.com/2.ts 61 | #EXT-X-CUE-IN 62 | `; 63 | 64 | t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected)); 65 | }); 66 | 67 | test("#EXT-X-CUE-IN_03", t => { 68 | const {MediaPlaylist, Segment} = HLS.types; 69 | 70 | const segments = [...Array.from({length: 6})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10})); 71 | segments[0].markers.push({type: 'OUT', duration: 20}); 72 | segments[2].markers.push({type: 'IN'}); 73 | segments[4].markers.push({type: 'OUT', duration: 20}); 74 | 75 | const playlist = new MediaPlaylist({ 76 | playlistType: 'EVENT', 77 | targetDuration: 10, 78 | segments 79 | }); 80 | 81 | // For live media playlist, unclosed CUE-OUT is allowed. 82 | const expected = ` 83 | #EXTM3U 84 | #EXT-X-TARGETDURATION:10 85 | #EXT-X-PLAYLIST-TYPE:EVENT 86 | #EXT-X-CUE-OUT:DURATION=20 87 | #EXTINF:10, 88 | https://example.com/0.ts 89 | #EXTINF:10, 90 | https://example.com/1.ts 91 | #EXT-X-CUE-IN 92 | #EXTINF:10, 93 | https://example.com/2.ts 94 | #EXTINF:10, 95 | https://example.com/3.ts 96 | #EXT-X-CUE-OUT:DURATION=20 97 | #EXTINF:10, 98 | https://example.com/4.ts 99 | #EXTINF:10, 100 | https://example.com/5.ts 101 | `; 102 | 103 | t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected)); 104 | }); 105 | 106 | test("#EXT-X-CUE-IN_04", t => { 107 | const {MediaPlaylist, Segment} = HLS.types; 108 | 109 | const segments = [...Array.from({length: 6})].map((_, i) => new Segment({uri: `https://example.com/${i}.ts`, duration: 10})); 110 | segments[0].markers.push({type: 'OUT', duration: 20}); 111 | segments[2].markers.push({type: 'IN'}); 112 | segments[4].markers.push({type: 'OUT', duration: 20}); 113 | 114 | const playlist = new MediaPlaylist({ 115 | playlistType: 'VOD', 116 | targetDuration: 10, 117 | segments 118 | }); 119 | 120 | // For VOD media playlist, unclosed CUE-OUT is not allowed. 121 | // CUE-IN will be added. 122 | const expected = ` 123 | #EXTM3U 124 | #EXT-X-TARGETDURATION:10 125 | #EXT-X-PLAYLIST-TYPE:VOD 126 | #EXT-X-CUE-OUT:DURATION=20 127 | #EXTINF:10, 128 | https://example.com/0.ts 129 | #EXTINF:10, 130 | https://example.com/1.ts 131 | #EXT-X-CUE-IN 132 | #EXTINF:10, 133 | https://example.com/2.ts 134 | #EXTINF:10, 135 | https://example.com/3.ts 136 | #EXT-X-CUE-OUT:DURATION=20 137 | #EXTINF:10, 138 | https://example.com/4.ts 139 | #EXTINF:10, 140 | https://example.com/5.ts 141 | #EXT-X-CUE-IN 142 | `; 143 | 144 | t.is(HLS.stringify(playlist), utils.stripCommentsAndEmptyLines(expected)); 145 | }); 146 | 147 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.5_EXT-X-MAP.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | const utils = require('../../../../helpers/utils'); 4 | 5 | // It applies to every Media Segment that appears after it in the 6 | // Playlist until the next EXT-X-MAP tag or until the end of the 7 | // playlist. 8 | test('#EXT-X-MAP_01', t => { 9 | let playlist; 10 | // Until the end of the Playlist 11 | playlist = HLS.parse(` 12 | #EXTM3U 13 | #EXT-X-VERSION:6 14 | #EXT-X-TARGETDURATION:10 15 | #EXTINF:10, 16 | http://example.com/1 17 | #EXT-X-MAP:URI="http://example.com/map-1" 18 | #EXTINF:10, 19 | http://example.com/2 20 | #EXTINF:10, 21 | http://example.com/3 22 | `); 23 | t.falsy(playlist.segments[0].map); 24 | t.truthy(playlist.segments[1].map); 25 | t.truthy(playlist.segments[2].map); 26 | // Until the next EXT-X-MAP tag 27 | playlist = HLS.parse(` 28 | #EXTM3U 29 | #EXT-X-VERSION:6 30 | #EXT-X-TARGETDURATION:10 31 | #EXT-X-MAP:URI="http://example.com/map-1" 32 | #EXTINF:10, 33 | http://example.com/1 34 | #EXTINF:10, 35 | http://example.com/2 36 | #EXT-X-MAP:URI="http://example.com/map-2" 37 | #EXTINF:10, 38 | http://example.com/3 39 | `); 40 | t.is(playlist.segments[0].map.uri, 'http://example.com/map-1'); 41 | t.is(playlist.segments[1].map.uri, 'http://example.com/map-1'); 42 | t.is(playlist.segments[2].map.uri, 'http://example.com/map-2'); 43 | HLS.stringify(playlist); 44 | }); 45 | 46 | // URI: This attribute is REQUIRED. 47 | test('#EXT-X-MAP_02', t => { 48 | utils.parseFail(t, ` 49 | #EXTM3U 50 | #EXT-X-VERSION:6 51 | #EXT-X-TARGETDURATION:10 52 | #EXT-X-MAP:BYTERANGE="256@128" 53 | #EXTINF:10, 54 | http://example.com/1 55 | `); 56 | utils.bothPass(t, ` 57 | #EXTM3U 58 | #EXT-X-VERSION:6 59 | #EXT-X-TARGETDURATION:10 60 | #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128" 61 | #EXTINF:10, 62 | http://example.com/1 63 | `); 64 | }); 65 | 66 | // Use of the EXT-X-MAP tag in a Media Playlist that contains the 67 | // EXT-X-I-FRAMES-ONLY tag REQUIRES a compatibility version number of 5 68 | // or greater. 69 | // URI: This attribute is REQUIRED. 70 | test('#EXT-X-MAP_03', t => { 71 | utils.parseFail(t, ` 72 | #EXTM3U 73 | #EXT-X-VERSION:4 74 | #EXT-X-TARGETDURATION:10 75 | #EXT-X-I-FRAMES-ONLY 76 | #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128" 77 | #EXTINF:10, 78 | http://example.com/1 79 | `); 80 | utils.bothPass(t, ` 81 | #EXTM3U 82 | #EXT-X-VERSION:5 83 | #EXT-X-TARGETDURATION:10 84 | #EXT-X-I-FRAMES-ONLY 85 | #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128" 86 | #EXTINF:10, 87 | http://example.com/1 88 | `); 89 | }); 90 | 91 | // Use of the EXT-X-MAP tag in a Media Playlist that DOES 92 | // NOT contain the EXT-X-I-FRAMES-ONLY tag REQUIRES a compatibility 93 | // version number of 6 or greater. 94 | test('#EXT-X-MAP_04', t => { 95 | utils.parseFail(t, ` 96 | #EXTM3U 97 | #EXT-X-VERSION:5 98 | #EXT-X-TARGETDURATION:10 99 | #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128" 100 | #EXTINF:10, 101 | http://example.com/1 102 | `); 103 | utils.bothPass(t, ` 104 | #EXTM3U 105 | #EXT-X-VERSION:6 106 | #EXT-X-TARGETDURATION:10 107 | #EXT-X-MAP:URI="http://example.com/map-1",BYTERANGE="256@128" 108 | #EXTINF:10, 109 | http://example.com/1 110 | `); 111 | }); 112 | 113 | // The tag place should be preserved 114 | test('#EXT-X-MAP_05', t => { 115 | const sourceText = ` 116 | #EXTM3U 117 | #EXT-X-VERSION:6 118 | #EXT-X-TARGETDURATION:10 119 | #EXT-X-MAP:URI="http://example.com/map-1" 120 | #EXTINF:10, 121 | http://example.com/1 122 | #EXTINF:10, 123 | http://example.com/2 124 | #EXT-X-MAP:URI="http://example.com/map-2" 125 | #EXTINF:10, 126 | http://example.com/3 127 | #EXTINF:10, 128 | http://example.com/4 129 | `; 130 | const obj = HLS.parse(sourceText); 131 | const text = HLS.stringify(obj); 132 | t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); 133 | }); 134 | 135 | // The same tag can appear multiple times 136 | test('#EXT-X-MAP_06', t => { 137 | const sourceText = ` 138 | #EXTM3U 139 | #EXT-X-VERSION:6 140 | #EXT-X-TARGETDURATION:10 141 | #EXT-X-MAP:URI="http://example.com/map-1" 142 | #EXTINF:10, 143 | http://example.com/1 144 | #EXT-X-MAP:URI="http://example.com/map-2" 145 | #EXTINF:10, 146 | http://example.com/2 147 | #EXT-X-MAP:URI="http://example.com/map-1" 148 | #EXTINF:10, 149 | http://example.com/3 150 | #EXT-X-MAP:URI="http://example.com/map-2" 151 | #EXTINF:10, 152 | http://example.com/4 153 | `; 154 | const obj = HLS.parse(sourceText); 155 | const text = HLS.stringify(obj); 156 | t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); 157 | }); 158 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.2_Media-Segment-Tags/4.3.2.4_EXT-X-KEY.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | const utils = require('../../../../helpers/utils'); 4 | 5 | // It applies to every Media Segment that appears between 6 | // it and the next EXT-X-KEY tag in the Playlist file with the same 7 | // KEYFORMAT attribute (or the end of the Playlist file). 8 | test('#EXT-X-KEY_01', t => { 9 | let playlist; 10 | // Until the end of the Playlist file 11 | playlist = HLS.parse(` 12 | #EXTM3U 13 | #EXT-X-TARGETDURATION:10 14 | #EXTINF:10, 15 | http://example.com/1 16 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" 17 | #EXTINF:10, 18 | http://example.com/2 19 | #EXTINF:10, 20 | http://example.com/3 21 | `); 22 | t.falsy(playlist.segments[0].key); 23 | t.truthy(playlist.segments[1].key); 24 | t.truthy(playlist.segments[2].key); 25 | // Until the next EXT-X-KEY tag in the Playlist file with the same 26 | // KEYFORMAT attribute 27 | playlist = HLS.parse(` 28 | #EXTM3U 29 | #EXT-X-VERSION:5 30 | #EXT-X-TARGETDURATION:10 31 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity" 32 | #EXTINF:10, 33 | http://example.com/1 34 | #EXTINF:10, 35 | http://example.com/2 36 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity" 37 | #EXTINF:10, 38 | http://example.com/3 39 | `); 40 | t.is(playlist.segments[0].key.uri, 'http://example.com/key-1'); 41 | t.is(playlist.segments[1].key.uri, 'http://example.com/key-1'); 42 | t.is(playlist.segments[2].key.uri, 'http://example.com/key-2'); 43 | }); 44 | 45 | // METHOD: This attribute is REQUIRED. 46 | test('#EXT-X-KEY_02', t => { 47 | utils.parseFail(t, ` 48 | #EXTM3U 49 | #EXT-X-TARGETDURATION:10 50 | #EXT-X-KEY:URI="http://example.com" 51 | #EXTINF:10, 52 | http://example.com/2 53 | `); 54 | utils.bothPass(t, ` 55 | #EXTM3U 56 | #EXT-X-TARGETDURATION:10 57 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" 58 | #EXTINF:10, 59 | http://example.com/2 60 | `); 61 | }); 62 | 63 | // If the encryption method is NONE, other attributes 64 | // MUST NOT be present. 65 | test('#EXT-X-KEY_03', t => { 66 | utils.parseFail(t, ` 67 | #EXTM3U 68 | #EXT-X-TARGETDURATION:10 69 | #EXT-X-KEY:METHOD=NONE,URI="http://example.com" 70 | #EXTINF:10, 71 | http://example.com/2 72 | `); 73 | }); 74 | 75 | // URI: This attribute is REQUIRED unless the METHOD is NONE. 76 | test('#EXT-X-KEY_04', t => { 77 | utils.parseFail(t, ` 78 | #EXTM3U 79 | #EXT-X-TARGETDURATION:10 80 | #EXT-X-KEY:METHOD=AES-128 81 | #EXTINF:10, 82 | http://example.com/2 83 | `); 84 | utils.bothPass(t, ` 85 | #EXTM3U 86 | #EXT-X-TARGETDURATION:10 87 | #EXT-X-KEY:METHOD=NONE 88 | #EXTINF:10, 89 | http://example.com/2 90 | `); 91 | }); 92 | 93 | // Use of the IV attribute REQUIRES a compatibility version number of 94 | // 2 or greater. 95 | test('#EXT-X-KEY_05', t => { 96 | utils.parseFail(t, ` 97 | #EXTM3U 98 | #EXT-X-VERSION:1 99 | #EXT-X-TARGETDURATION:10 100 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 101 | #EXTINF:10, 102 | http://example.com/2 103 | `); 104 | const playlist = utils.parsePass(t, ` 105 | #EXTM3U 106 | #EXT-X-VERSION:2 107 | #EXT-X-TARGETDURATION:10 108 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 109 | #EXTINF:10, 110 | http://example.com/2 111 | `); 112 | t.is(playlist.segments[0].key.iv.length, 16); 113 | }); 114 | 115 | // The tag place should be preserved 116 | test('#EXT-X-KEY_06', t => { 117 | const sourceText = ` 118 | #EXTM3U 119 | #EXT-X-VERSION:5 120 | #EXT-X-TARGETDURATION:10 121 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity" 122 | #EXTINF:10, 123 | http://example.com/1 124 | #EXTINF:10, 125 | http://example.com/2 126 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity" 127 | #EXTINF:10, 128 | http://example.com/3 129 | #EXTINF:10, 130 | http://example.com/4 131 | `; 132 | const obj = HLS.parse(sourceText); 133 | const text = HLS.stringify(obj); 134 | t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); 135 | }); 136 | 137 | // The same tag can appear multiple times 138 | test('#EXT-X-KEY_07', t => { 139 | const sourceText = ` 140 | #EXTM3U 141 | #EXT-X-VERSION:5 142 | #EXT-X-TARGETDURATION:10 143 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity" 144 | #EXTINF:10, 145 | http://example.com/1 146 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity" 147 | #EXTINF:10, 148 | http://example.com/2 149 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-1",KEYFORMAT="identity" 150 | #EXTINF:10, 151 | http://example.com/3 152 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com/key-2",KEYFORMAT="identity" 153 | #EXTINF:10, 154 | http://example.com/4 155 | `; 156 | const obj = HLS.parse(sourceText); 157 | const text = HLS.stringify(obj); 158 | t.is(text, utils.stripCommentsAndEmptyLines(sourceText)); 159 | }); 160 | -------------------------------------------------------------------------------- /test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/02_EXT-X-PART-INF.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../helpers/utils'); 3 | const HLS = require('../../../..'); 4 | 5 | // EXT-X-PART-INF provides information about HLS Partial Segments in the Playlist. It is 6 | // required if a Playlist contains one or more EXT-X-PART tags. 7 | test('#EXT-X-PART-INF_01', t => { 8 | utils.bothPass(t, ` 9 | #EXTM3U 10 | #EXT-X-TARGETDURATION:2 11 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 12 | #EXT-X-PART-INF:PART-TARGET=0.2 13 | #EXTINF:2, 14 | fs240.mp4 15 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 16 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 17 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 18 | `); 19 | utils.parseFail(t, ` 20 | #EXTM3U 21 | #EXT-X-TARGETDURATION:2 22 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 23 | #EXTINF:2, 24 | fs240.mp4 25 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 26 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 27 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 28 | `); 29 | }); 30 | 31 | // PART-TARGET=: (mandatory) 32 | test('#EXT-X-PART-INF_02', t => { 33 | utils.bothPass(t, ` 34 | #EXTM3U 35 | #EXT-X-TARGETDURATION:2 36 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 37 | #EXT-X-PART-INF:PART-TARGET=0.2 38 | #EXTINF:2, 39 | fs240.mp4 40 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 41 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 42 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 43 | `); 44 | utils.parseFail(t, ` 45 | #EXTM3U 46 | #EXT-X-TARGETDURATION:2 47 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 48 | #EXT-X-PART-INF 49 | #EXTINF:2, 50 | fs240.mp4 51 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 52 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 53 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 54 | `); 55 | }); 56 | 57 | // PART-TARGET=: (mandatory) Indicates the part target duration in floating-point seconds 58 | // and is the maximum duration of any Partial Segment. 59 | test('#EXT-X-PART-INF_03', t => { 60 | utils.bothPass(t, ` 61 | #EXTM3U 62 | #EXT-X-TARGETDURATION:2 63 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 64 | #EXT-X-PART-INF:PART-TARGET=0.2 65 | #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@0 66 | #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@20000 67 | #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@40000 68 | #EXTINF:2, 69 | fs240.mp4 70 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 71 | #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 72 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 73 | `); 74 | utils.parseFail(t, ` 75 | #EXTM3U 76 | #EXT-X-TARGETDURATION:2 77 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 78 | #EXT-X-PART-INF:PART-TARGET=0.17 79 | #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@0 80 | #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@20000 81 | #EXT-X-PART:DURATION=0.17,URI="fs240.mp4",BYTERANGE=20000@40000 82 | #EXTINF:2, 83 | fs240.mp4 84 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 85 | #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 86 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 87 | `); 88 | }); 89 | 90 | // All Partial Segments except the last part of a segment 91 | // must have a duration of at least 85% of PART-TARGET. 92 | test('#EXT-X-PART-INF_04', t => { 93 | utils.bothPass(t, ` 94 | #EXTM3U 95 | #EXT-X-TARGETDURATION:2 96 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 97 | #EXT-X-PART-INF:PART-TARGET=0.2 98 | #EXTINF:2, 99 | fs240.mp4 100 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 101 | #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=23000@20000 102 | #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=18000@43000 103 | #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@61000 104 | #EXT-X-PART:DURATION=0.05,URI="fs241.mp4",BYTERANGE=10000@81000 105 | #EXTINF:2, 106 | fs241.mp4 107 | `); 108 | utils.parseFail(t, ` 109 | #EXTM3U 110 | #EXT-X-TARGETDURATION:2 111 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 112 | #EXT-X-PART-INF:PART-TARGET=0.2 113 | #EXTINF:2, 114 | fs240.mp4 115 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 116 | #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=23000@20000 117 | #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=18000@43000 118 | #EXT-X-PART:DURATION=0.16,URI="fs241.mp4",BYTERANGE=20000@61000 119 | #EXT-X-PART:DURATION=0.05,URI="fs241.mp4",BYTERANGE=10000@81000 120 | #EXTINF:2, 121 | fs241.mp4 122 | `); 123 | }); 124 | 125 | test('#EXT-X-PART-INF_05', t => { 126 | const {partTargetDuration} = HLS.parse(` 127 | #EXTM3U 128 | #EXT-X-TARGETDURATION:2 129 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 130 | #EXT-X-PART-INF:PART-TARGET=0.2 131 | #EXTINF:2, 132 | fs240.mp4 133 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 134 | #EXT-X-PART:DURATION=0.17,URI="fs241.mp4",BYTERANGE=20000@20000 135 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 136 | `); 137 | 138 | t.is(partTargetDuration, 0.2); 139 | }); 140 | -------------------------------------------------------------------------------- /test/fixtures/objects/Streaming-Examples_bipbop_16x9_variant.js: -------------------------------------------------------------------------------- 1 | const {MasterPlaylist, Variant, Rendition} = require('../../../types'); 2 | 3 | const renditions = { 4 | bipbop_audio: [ 5 | new Rendition({ 6 | type: 'AUDIO', 7 | groupId: 'bipbop_audio', 8 | language: 'eng', 9 | name: 'BipBop Audio 1', 10 | autoselect: true, 11 | isDefault: true 12 | }), 13 | new Rendition({ 14 | type: 'AUDIO', 15 | uri: 'alternate_audio_aac/prog_index.m3u8', 16 | groupId: 'bipbop_audio', 17 | language: 'eng', 18 | name: 'BipBop Audio 2', 19 | autoselect: false, 20 | isDefault: false 21 | }) 22 | ], 23 | subs: [ 24 | new Rendition({ 25 | type: 'SUBTITLES', 26 | uri: 'subtitles/eng/prog_index.m3u8', 27 | groupId: 'subs', 28 | language: 'en', 29 | name: 'English', 30 | autoselect: true, 31 | isDefault: true, 32 | forced: false, 33 | characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound' 34 | }), 35 | new Rendition({ 36 | type: 'SUBTITLES', 37 | uri: 'subtitles/eng_forced/prog_index.m3u8', 38 | groupId: 'subs', 39 | language: 'en', 40 | name: 'English (Forced)', 41 | autoselect: false, 42 | isDefault: false, 43 | forced: true 44 | }), 45 | new Rendition({ 46 | type: 'SUBTITLES', 47 | uri: 'subtitles/fra/prog_index.m3u8', 48 | groupId: 'subs', 49 | language: 'fr', 50 | name: 'Français', 51 | autoselect: true, 52 | isDefault: false, 53 | forced: false, 54 | characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound' 55 | }), 56 | new Rendition({ 57 | type: 'SUBTITLES', 58 | uri: 'subtitles/fra_forced/prog_index.m3u8', 59 | groupId: 'subs', 60 | language: 'fr', 61 | name: 'Français (Forced)', 62 | autoselect: false, 63 | isDefault: false, 64 | forced: true 65 | }), 66 | new Rendition({ 67 | type: 'SUBTITLES', 68 | uri: 'subtitles/spa/prog_index.m3u8', 69 | groupId: 'subs', 70 | language: 'es', 71 | name: 'Español', 72 | autoselect: true, 73 | isDefault: false, 74 | forced: false, 75 | characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound' 76 | }), 77 | new Rendition({ 78 | type: 'SUBTITLES', 79 | uri: 'subtitles/spa_forced/prog_index.m3u8', 80 | groupId: 'subs', 81 | language: 'es', 82 | name: 'Español (Forced)', 83 | autoselect: false, 84 | isDefault: false, 85 | forced: true 86 | }), 87 | new Rendition({ 88 | type: 'SUBTITLES', 89 | uri: 'subtitles/jpn/prog_index.m3u8', 90 | groupId: 'subs', 91 | language: 'ja', 92 | name: '日本語', 93 | autoselect: true, 94 | isDefault: false, 95 | forced: false, 96 | characteristics: 'public.accessibility.transcribes-spoken-dialog, public.accessibility.describes-music-and-sound' 97 | }), 98 | new Rendition({ 99 | type: 'SUBTITLES', 100 | uri: 'subtitles/jpn_forced/prog_index.m3u8', 101 | groupId: 'subs', 102 | language: 'ja', 103 | name: '日本語 (Forced)', 104 | autoselect: false, 105 | isDefault: false, 106 | forced: true 107 | }) 108 | ] 109 | }; 110 | 111 | const playlist = new MasterPlaylist({ 112 | variants: createVariants() 113 | }); 114 | 115 | function createVariants() { 116 | const variants = []; 117 | variants.push(new Variant({ 118 | uri: 'gear1/prog_index.m3u8', 119 | bandwidth: 263851, 120 | codecs: 'mp4a.40.2, avc1.4d400d', 121 | resolution: {width: 416, height: 234}, 122 | audio: renditions.bipbop_audio, 123 | subtitles: renditions.subs 124 | })); 125 | variants.push(new Variant({ 126 | uri: 'gear1/iframe_index.m3u8', 127 | isIFrameOnly: true, 128 | bandwidth: 28451, 129 | codecs: 'avc1.4d400d' 130 | })); 131 | variants.push(new Variant({ 132 | uri: 'gear2/prog_index.m3u8', 133 | bandwidth: 577610, 134 | codecs: 'mp4a.40.2, avc1.4d401e', 135 | resolution: {width: 640, height: 360}, 136 | audio: renditions.bipbop_audio, 137 | subtitles: renditions.subs 138 | })); 139 | variants.push(new Variant({ 140 | uri: 'gear2/iframe_index.m3u8', 141 | isIFrameOnly: true, 142 | bandwidth: 181534, 143 | codecs: 'avc1.4d401e' 144 | })); 145 | variants.push(new Variant({ 146 | uri: 'gear3/prog_index.m3u8', 147 | bandwidth: 915905, 148 | codecs: 'mp4a.40.2, avc1.4d401f', 149 | resolution: {width: 960, height: 540}, 150 | audio: renditions.bipbop_audio, 151 | subtitles: renditions.subs 152 | })); 153 | variants.push(new Variant({ 154 | uri: 'gear3/iframe_index.m3u8', 155 | isIFrameOnly: true, 156 | bandwidth: 297056, 157 | codecs: 'avc1.4d401f' 158 | })); 159 | variants.push(new Variant({ 160 | uri: 'gear4/prog_index.m3u8', 161 | bandwidth: 1030138, 162 | codecs: 'mp4a.40.2, avc1.4d401f', 163 | resolution: {width: 1280, height: 720}, 164 | audio: renditions.bipbop_audio, 165 | subtitles: renditions.subs 166 | })); 167 | variants.push(new Variant({ 168 | uri: 'gear4/iframe_index.m3u8', 169 | isIFrameOnly: true, 170 | bandwidth: 339492, 171 | codecs: 'avc1.4d401f' 172 | })); 173 | variants.push(new Variant({ 174 | uri: 'gear5/prog_index.m3u8', 175 | bandwidth: 1924009, 176 | codecs: 'mp4a.40.2, avc1.4d401f', 177 | resolution: {width: 1920, height: 1080}, 178 | audio: renditions.bipbop_audio, 179 | subtitles: renditions.subs 180 | })); 181 | variants.push(new Variant({ 182 | uri: 'gear5/iframe_index.m3u8', 183 | isIFrameOnly: true, 184 | bandwidth: 669554, 185 | codecs: 'avc1.4d401f' 186 | })); 187 | variants.push(new Variant({ 188 | uri: 'gear0/prog_index.m3u8', 189 | bandwidth: 41457, 190 | codecs: 'mp4a.40.2', 191 | audio: renditions.bipbop_audio, 192 | subtitles: renditions.subs 193 | })); 194 | return variants; 195 | } 196 | 197 | module.exports = playlist; 198 | -------------------------------------------------------------------------------- /utils.ts: -------------------------------------------------------------------------------- 1 | 2 | type Options = { 3 | strictMode?: boolean, 4 | allowClosedCaptionsNone?: boolean, 5 | silent?: boolean 6 | }; 7 | 8 | let options: Options = {}; 9 | 10 | function THROW(err: Error) { 11 | if (!options.strictMode) { 12 | if (!options.silent) { 13 | console.error(err.message); 14 | } 15 | return; 16 | } 17 | throw err; 18 | } 19 | 20 | function ASSERT(msg: string, ...options: boolean[]) { 21 | for (const [index, param] of options.entries()) { 22 | if (!param) { 23 | THROW(new Error(`${msg} : Failed at [${index}]`)); 24 | } 25 | } 26 | } 27 | 28 | function CONDITIONALASSERT(...options) { 29 | for (const [index, [cond, param]] of options.entries()) { 30 | if (!cond) { 31 | continue; 32 | } 33 | if (!param) { 34 | THROW(new Error(`Conditional Assert : Failed at [${index}]`)); 35 | } 36 | } 37 | } 38 | 39 | function PARAMCHECK(...options) { 40 | for (const [index, param] of options.entries()) { 41 | if (param === undefined) { 42 | THROW(new Error(`Param Check : Failed at [${index}]`)); 43 | } 44 | } 45 | } 46 | 47 | function CONDITIONALPARAMCHECK(...options) { 48 | for (const [index, [cond, param]] of options.entries()) { 49 | if (!cond) { 50 | continue; 51 | } 52 | if (param === undefined) { 53 | THROW(new Error(`Conditional Param Check : Failed at [${index}]`)); 54 | } 55 | } 56 | } 57 | 58 | function INVALIDPLAYLIST(msg: string) { 59 | THROW(new Error(`Invalid Playlist : ${msg}`)); 60 | } 61 | 62 | function toNumber(str: string, radix = 10) { 63 | if (typeof str === 'number') { 64 | return str; 65 | } 66 | const num = radix === 10 ? Number.parseFloat(str) : Number.parseInt(str, radix); 67 | if (Number.isNaN(num)) { 68 | return 0; 69 | } 70 | return num; 71 | } 72 | 73 | function hexToByteSequence(str: string): Uint8Array { 74 | if (str.startsWith('0x') || str.startsWith('0X')) { 75 | str = str.slice(2); 76 | } 77 | const numArray = new Uint8Array(str.length / 2); 78 | for (let i = 0; i < str.length; i += 2) { 79 | numArray[i / 2] = Number.parseInt(str.slice(i, i + 2), 16); 80 | } 81 | return numArray; 82 | } 83 | 84 | function byteSequenceToHex(sequence: ArrayBuffer, start = 0, end = sequence.byteLength) { 85 | if (end <= start) { 86 | THROW(new Error(`end must be larger than start : start=${start}, end=${end}`)); 87 | } 88 | const array: string[] = []; 89 | for (let i = start; i < end; i++) { 90 | array.push(`0${(sequence[i] & 0xFF).toString(16).toUpperCase()}`.slice(-2)); 91 | } 92 | return `0x${array.join('')}`; 93 | } 94 | 95 | function tryCatch(body: () => T, errorHandler: (err: unknown) => T): T { 96 | try { 97 | return body(); 98 | } catch (err) { 99 | return errorHandler(err); 100 | } 101 | } 102 | 103 | function splitAt(str: string, delimiter: string, index = 0): [string] | [string, string] { 104 | let lastDelimiterPos = -1; 105 | for (let i = 0, j = 0; i < str.length; i++) { 106 | if (str[i] === delimiter) { 107 | if (j++ === index) { 108 | return [str.slice(0, i), str.slice(i + 1)]; 109 | } 110 | lastDelimiterPos = i; 111 | } 112 | } 113 | if (lastDelimiterPos !== -1) { 114 | return [str.slice(0, lastDelimiterPos), str.slice(lastDelimiterPos + 1)]; 115 | } 116 | return [str]; 117 | } 118 | 119 | function trim(str: string | undefined, char = ' ') { 120 | if (!str) { 121 | return str; 122 | } 123 | str = str.trim(); 124 | if (char === ' ') { 125 | return str; 126 | } 127 | if (str.startsWith(char)) { 128 | str = str.slice(1); 129 | } 130 | if (str.endsWith(char)) { 131 | str = str.slice(0, -1); 132 | } 133 | return str; 134 | } 135 | 136 | function splitByCommaWithPreservingQuotes(str: string) { 137 | const list: string[] = []; 138 | let doParse = true; 139 | let start = 0; 140 | const prevQuotes: string[] = []; 141 | for (let i = 0; i < str.length; i++) { 142 | const curr = str[i]; 143 | if (doParse && curr === ',') { 144 | list.push(str.slice(start, i).trim()); 145 | start = i + 1; 146 | continue; 147 | } 148 | if (curr === '"' || curr === '\'') { 149 | if (doParse) { 150 | prevQuotes.push(curr); 151 | doParse = false; 152 | } else if (curr === prevQuotes.at(-1)) { 153 | prevQuotes.pop(); 154 | doParse = true; 155 | } else { 156 | prevQuotes.push(curr); 157 | } 158 | } 159 | } 160 | list.push(str.slice(start).trim()); 161 | return list; 162 | } 163 | 164 | function camelify(str: string) { 165 | const array: string[] = []; 166 | let nextUpper = false; 167 | for (const ch of str) { 168 | if (ch === '-' || ch === '_') { 169 | nextUpper = true; 170 | continue; 171 | } 172 | if (nextUpper) { 173 | array.push(ch.toUpperCase()); 174 | nextUpper = false; 175 | continue; 176 | } 177 | array.push(ch.toLowerCase()); 178 | } 179 | return array.join(''); 180 | } 181 | 182 | function formatDate(date: Date) { 183 | const YYYY = date.getUTCFullYear(); 184 | const MM = ('0' + (date.getUTCMonth() + 1)).slice(-2); 185 | const DD = ('0' + date.getUTCDate()).slice(-2); 186 | const hh = ('0' + date.getUTCHours()).slice(-2); 187 | const mm = ('0' + date.getUTCMinutes()).slice(-2); 188 | const ss = ('0' + date.getUTCSeconds()).slice(-2); 189 | const msc = ('00' + date.getUTCMilliseconds()).slice(-3); 190 | return `${YYYY}-${MM}-${DD}T${hh}:${mm}:${ss}.${msc}Z`; 191 | } 192 | 193 | function hasOwnProp(obj: object, propName: string): boolean { 194 | return Object.hasOwn(obj, propName); 195 | } 196 | 197 | function setOptions(newOptions: Partial = {}): void { 198 | options = Object.assign(options, newOptions); 199 | } 200 | 201 | function getOptions(): Options { 202 | return Object.assign({}, options); 203 | } 204 | 205 | export { 206 | THROW, 207 | ASSERT, 208 | CONDITIONALASSERT, 209 | PARAMCHECK, 210 | CONDITIONALPARAMCHECK, 211 | INVALIDPLAYLIST, 212 | toNumber, 213 | hexToByteSequence, 214 | byteSequenceToHex, 215 | tryCatch, 216 | splitAt, 217 | trim, 218 | splitByCommaWithPreservingQuotes, 219 | camelify, 220 | formatDate, 221 | hasOwnProp, 222 | setOptions, 223 | getOptions 224 | }; 225 | -------------------------------------------------------------------------------- /test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/01_EXT-X-SERVER-CONTROL.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../helpers/utils'); 3 | const HLS = require('../../../..'); 4 | 5 | // CAN-BLOCK-RELOAD=YES: ... 6 | // It is mandatory for Low-Latency HLS. 7 | test('#EXT-X-SERVER-CONTROL_01', t => { 8 | utils.bothPass(t, ` 9 | #EXTM3U 10 | #EXT-X-TARGETDURATION:2 11 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0 12 | #EXTINF:2, 13 | fs240.mp4 14 | #EXTINF:2, 15 | fs241.mp4 16 | `); 17 | utils.parseFail(t, ` 18 | #EXTM3U 19 | #EXT-X-TARGETDURATION:2 20 | #EXT-X-SERVER-CONTROL:CAN-SKIP-UNTIL=12.0 21 | #EXTINF:2, 22 | fs240.mp4 23 | #EXTINF:2, 24 | fs241.mp4 25 | `); 26 | }); 27 | 28 | // CAN-SKIP-UNTIL=: (optional) 29 | test('#EXT-X-SERVER-CONTROL_02', t => { 30 | utils.bothPass(t, ` 31 | #EXTM3U 32 | #EXT-X-TARGETDURATION:2 33 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0 34 | #EXTINF:2, 35 | fs240.mp4 36 | #EXTINF:2, 37 | fs241.mp4 38 | `); 39 | utils.bothPass(t, ` 40 | #EXTM3U 41 | #EXT-X-TARGETDURATION:2 42 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES 43 | #EXTINF:2, 44 | fs240.mp4 45 | #EXTINF:2, 46 | fs241.mp4 47 | `); 48 | }); 49 | 50 | // CAN-SKIP-UNTIL=: ... 51 | // The Skip Boundary must be at least six times the EXT-X-TARGETDURATION. 52 | test('#EXT-X-SERVER-CONTROL_03', t => { 53 | utils.bothPass(t, ` 54 | #EXTM3U 55 | #EXT-X-TARGETDURATION:2 56 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0 57 | #EXTINF:2, 58 | fs240.mp4 59 | #EXTINF:2, 60 | fs241.mp4 61 | `); 62 | utils.parseFail(t, ` 63 | #EXTM3U 64 | #EXT-X-TARGETDURATION:2 65 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=11.9 66 | #EXTINF:2, 67 | fs240.mp4 68 | #EXTINF:2, 69 | fs241.mp4 70 | `); 71 | }); 72 | 73 | // HOLD-BACK=: (optional) 74 | test('#EXT-X-SERVER-CONTROL_04', t => { 75 | utils.bothPass(t, ` 76 | #EXTM3U 77 | #EXT-X-TARGETDURATION:2 78 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=6.0 79 | #EXTINF:2, 80 | fs240.mp4 81 | #EXTINF:2, 82 | fs241.mp4 83 | `); 84 | utils.bothPass(t, ` 85 | #EXTM3U 86 | #EXT-X-TARGETDURATION:2 87 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES 88 | #EXTINF:2, 89 | fs240.mp4 90 | #EXTINF:2, 91 | fs241.mp4 92 | `); 93 | }); 94 | 95 | // HOLD-BACK=: ... 96 | // Its value is a floating-point number of seconds and must be at least 97 | // three times the EXT-X-TARGETDURATION. 98 | test('#EXT-X-SERVER-CONTROL_05', t => { 99 | utils.bothPass(t, ` 100 | #EXTM3U 101 | #EXT-X-TARGETDURATION:2 102 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=6.0 103 | #EXTINF:2, 104 | fs240.mp4 105 | #EXTINF:2, 106 | fs241.mp4 107 | `); 108 | utils.parseFail(t, ` 109 | #EXTM3U 110 | #EXT-X-TARGETDURATION:2 111 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,HOLD-BACK=5.9 112 | #EXTINF:2, 113 | fs240.mp4 114 | #EXTINF:2, 115 | fs241.mp4 116 | `); 117 | }); 118 | 119 | // PART-HOLD-BACK=: ... 120 | // It is mandatory if the Playlist contains EXT-X-PART tags. 121 | test('#EXT-X-SERVER-CONTROL_06', t => { 122 | utils.bothPass(t, ` 123 | #EXTM3U 124 | #EXT-X-TARGETDURATION:2 125 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.6 126 | #EXT-X-PART-INF:PART-TARGET=0.2 127 | #EXTINF:2, 128 | fs240.mp4 129 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 130 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 131 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 132 | `); 133 | utils.parseFail(t, ` 134 | #EXTM3U 135 | #EXT-X-TARGETDURATION:2 136 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES 137 | #EXT-X-PART-INF:PART-TARGET=0.2 138 | #EXTINF:2, 139 | fs240.mp4 140 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 141 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 142 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 143 | `); 144 | }); 145 | 146 | // PART-HOLD-BACK=: ... 147 | // This attribute's value is a floating-point number of seconds and must be 148 | // at least PART-TARGET. 149 | test('#EXT-X-SERVER-CONTROL_07', t => { 150 | utils.bothPass(t, ` 151 | #EXTM3U 152 | #EXT-X-TARGETDURATION:2 153 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.2 154 | #EXT-X-PART-INF:PART-TARGET=0.2 155 | #EXTINF:2, 156 | fs240.mp4 157 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 158 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 159 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 160 | `); 161 | utils.parseFail(t, ` 162 | #EXTM3U 163 | #EXT-X-TARGETDURATION:2 164 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,PART-HOLD-BACK=0.19 165 | #EXT-X-PART-INF:PART-TARGET=0.2 166 | #EXTINF:2, 167 | fs240.mp4 168 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 169 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 170 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 171 | `); 172 | }); 173 | 174 | test('#EXT-X-SERVER-CONTROL_08', t => { 175 | const {lowLatencyCompatibility} = HLS.parse(` 176 | #EXTM3U 177 | #EXT-X-TARGETDURATION:2 178 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 179 | #EXT-X-PART-INF:PART-TARGET=0.2 180 | #EXTINF:2, 181 | fs240.mp4 182 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 183 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 184 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 185 | `); 186 | 187 | t.truthy(lowLatencyCompatibility); 188 | t.is(lowLatencyCompatibility.canBlockReload, true); 189 | t.is(lowLatencyCompatibility.canSkipUntil, 12); 190 | t.is(lowLatencyCompatibility.holdBack, 6); 191 | t.is(lowLatencyCompatibility.partHoldBack, 0.2); 192 | }); 193 | -------------------------------------------------------------------------------- /test/spec/utils.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const rewire = require('rewire'); 3 | const utils = require('../../utils'); 4 | 5 | utils.setOptions({strictMode: true}); 6 | 7 | test('utils.THROW', t => { 8 | try { 9 | utils.THROW(new Error('abc')); 10 | } catch (err) { 11 | t.truthy(err); 12 | t.is(err.message, 'abc'); 13 | } 14 | }); 15 | 16 | test('utils.ASSERT', t => { 17 | utils.ASSERT('No error occurred', 1, 2, 3); 18 | try { 19 | utils.ASSERT('Error occurred', 1, 2, false); 20 | } catch (err) { 21 | t.truthy(err); 22 | t.is(err.message, 'Error occurred : Failed at [2]'); 23 | } 24 | }); 25 | 26 | test('utils.CONDITIONALASSERT', t => { 27 | utils.CONDITIONALASSERT([true, 1], [true, 2], [true, 3]); 28 | utils.CONDITIONALASSERT([false, 0], [false, 1], [false, 2]); 29 | try { 30 | utils.CONDITIONALASSERT([false, 0], [true, 1], [true, 0]); 31 | } catch (err) { 32 | t.truthy(err); 33 | t.is(err.message, 'Conditional Assert : Failed at [2]'); 34 | } 35 | }); 36 | 37 | test('utils.PARAMCHECK', t => { 38 | utils.PARAMCHECK(1, 2, 3); 39 | try { 40 | utils.PARAMCHECK(1, 2, undefined); 41 | } catch (err) { 42 | t.truthy(err); 43 | t.is(err.message, 'Param Check : Failed at [2]'); 44 | } 45 | }); 46 | 47 | test('utils.CONDITIONALPARAMCHECK', t => { 48 | utils.CONDITIONALPARAMCHECK([true, 1], [true, 2], [true, 3]); 49 | utils.CONDITIONALPARAMCHECK([false, undefined], [false, 1], [false, 2]); 50 | try { 51 | utils.CONDITIONALPARAMCHECK([false, undefined], [true, 1], [true, undefined]); 52 | } catch (err) { 53 | t.truthy(err); 54 | t.is(err.message, 'Conditional Param Check : Failed at [2]'); 55 | } 56 | }); 57 | 58 | test('utils.toNumber', t => { 59 | t.is(utils.toNumber('123'), 123); 60 | t.is(utils.toNumber(123), 123); 61 | t.is(utils.toNumber('abc'), 0); 62 | t.is(utils.toNumber('8bc'), 8); 63 | }); 64 | 65 | test('utils.hexToByteSequence', t => { 66 | t.deepEqual(utils.hexToByteSequence('0x000000'), new Uint8Array([0, 0, 0])); 67 | t.deepEqual(utils.hexToByteSequence('0xFFFFFF'), new Uint8Array([255, 255, 255])); 68 | t.deepEqual(utils.hexToByteSequence('FFFFFF'), new Uint8Array([255, 255, 255])); 69 | }); 70 | 71 | test('utils.byteSequenceToHex', t => { 72 | t.is(utils.byteSequenceToHex(new Uint8Array([0, 0, 0])), '0x000000'); 73 | t.is(utils.byteSequenceToHex(new Uint8Array([255, 255, 255])), '0xFFFFFF'); 74 | t.is(utils.byteSequenceToHex(new Uint8Array([255, 255, 256])), '0xFFFF00'); 75 | }); 76 | 77 | test('utils.tryCatch', t => { 78 | let result = utils.tryCatch( 79 | () => { 80 | return 1; 81 | }, 82 | () => { 83 | return 0; 84 | } 85 | ); 86 | t.is(result, 1); 87 | result = utils.tryCatch( 88 | () => { 89 | return JSON.parse('{{'); 90 | }, 91 | () => { 92 | return 0; 93 | } 94 | ); 95 | t.is(result, 0); 96 | t.throws(() => { 97 | utils.tryCatch( 98 | () => { 99 | return JSON.parse('{{'); 100 | }, 101 | () => { 102 | return JSON.parse('}}'); 103 | } 104 | ); 105 | }); 106 | }); 107 | 108 | test('utils.splitAt', t => { 109 | t.deepEqual(utils.splitAt('a=1', '='), ['a', '1']); 110 | t.deepEqual(utils.splitAt('a=1=2', '='), ['a', '1=2']); 111 | t.deepEqual(utils.splitAt('a=1=2=3', '='), ['a', '1=2=3']); 112 | t.deepEqual(utils.splitAt('a=1=2=3', '=', 0), ['a', '1=2=3']); 113 | t.deepEqual(utils.splitAt('a=1=2=3', '=', 1), ['a=1', '2=3']); 114 | t.deepEqual(utils.splitAt('a=1=2=3', '=', 2), ['a=1=2', '3']); 115 | t.deepEqual(utils.splitAt('a=1=2=3', '=', -1), ['a=1=2', '3']); 116 | }); 117 | 118 | test('utils.trim', t => { 119 | t.is(utils.trim(' abc '), 'abc'); 120 | t.is(utils.trim(' abc ', ' '), 'abc'); 121 | t.is(utils.trim('"abc"', '"'), 'abc'); 122 | t.is(utils.trim('abc:', ':'), 'abc'); 123 | t.is(utils.trim('abc'), 'abc'); 124 | t.is(utils.trim(' "abc" ', '"'), 'abc'); 125 | }); 126 | 127 | test('utils.splitWithPreservingQuotes', t => { 128 | t.deepEqual(utils.splitByCommaWithPreservingQuotes('abc=123, def="4,5,6", ghi=78=9, jkl="abc\'123\'def"'), ['abc=123', 'def="4,5,6"', 'ghi=78=9', 'jkl="abc\'123\'def"']); 129 | }); 130 | 131 | test('utls.camelify', t => { 132 | const props = ['caption', 'Caption', 'captioN', 'CAPTION', 'closed-captions', 'closed_captions', 'CLOSED-CAPTIONS']; 133 | const results = ['caption', 'caption', 'caption', 'caption', 'closedCaptions', 'closedCaptions', 'closedCaptions']; 134 | t.deepEqual(props.map(p => utils.camelify(p)), results); 135 | }); 136 | 137 | test('utils.formatDate', t => { 138 | const DATE = '2014-03-05T11:15:00.000Z'; 139 | t.is(utils.formatDate(new Date(DATE)), DATE); 140 | const LOCALDATE = '2000-01-01T08:59:59.999+09:00'; 141 | const UTC = '1999-12-31T23:59:59.999Z'; 142 | t.is(utils.formatDate(new Date(LOCALDATE)), UTC); 143 | }); 144 | 145 | test('utils.setOptions/getOptions', t => { 146 | const params = {a: 1, b: 'b', c: [1, 2, 3], strictMode: true}; 147 | utils.setOptions(params); 148 | t.deepEqual(params, utils.getOptions()); 149 | params.strictMode = false; 150 | t.notDeepEqual(params, utils.getOptions()); 151 | t.is(utils.getOptions().strictMode, true); 152 | }); 153 | 154 | test('utils.THROW.strictMode', t => { 155 | const message = 'Error Message'; 156 | utils.setOptions({strictMode: false}); 157 | try { 158 | utils.THROW({message}); 159 | } catch { 160 | t.fail(); 161 | } 162 | utils.setOptions({strictMode: true}); 163 | try { 164 | utils.THROW({message}); 165 | t.fail(); 166 | } catch (e) { 167 | t.is(e.message, message); 168 | } 169 | t.pass(); 170 | }); 171 | 172 | test('utils.THROW.silent', t => { 173 | let silent = false; 174 | const utils = rewire('../../utils'); 175 | const errorHandler = msg => { 176 | if (silent) { 177 | t.is(msg, 'end'); 178 | } else { 179 | t.is(msg, message); 180 | } 181 | }; 182 | utils.__set__({ 183 | console: { 184 | error: errorHandler, 185 | log: console.log 186 | } 187 | }); 188 | const message = 'Error Message'; 189 | utils.setOptions({strictMode: false}); 190 | utils.THROW({message}); 191 | silent = true; 192 | utils.setOptions({silent}); 193 | utils.THROW({message}); 194 | console.error('end'); 195 | utils.setOptions({strictMode: true}); 196 | }); 197 | -------------------------------------------------------------------------------- /test/fixtures/m3u8/Streaming-Examples_img_bipbop_adv_example_ts_master.m3u8: -------------------------------------------------------------------------------- 1 | #EXTM3U 2 | #EXT-X-VERSION:6 3 | #EXT-X-INDEPENDENT-SEGMENTS 4 | 5 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud1",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="2",URI="a1/prog_index.m3u8" 6 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="sub1",NAME="English",DEFAULT=YES,AUTOSELECT=YES,FORCED=NO,LANGUAGE="en",URI="s1/en/prog_index.m3u8" 7 | #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc1",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",INSTREAM-ID="CC1" 8 | 9 | #EXT-X-STREAM-INF:BANDWIDTH=2227464,AVERAGE-BANDWIDTH=2218327,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=960x540,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 10 | v5/prog_index.m3u8 11 | #EXT-X-STREAM-INF:BANDWIDTH=8178040,AVERAGE-BANDWIDTH=8144656,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 12 | v9/prog_index.m3u8 13 | #EXT-X-STREAM-INF:BANDWIDTH=6453202,AVERAGE-BANDWIDTH=6307144,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 14 | v8/prog_index.m3u8 15 | #EXT-X-STREAM-INF:BANDWIDTH=5054232,AVERAGE-BANDWIDTH=4775338,CODECS="avc1.64002a,mp4a.40.2",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 16 | v7/prog_index.m3u8 17 | #EXT-X-STREAM-INF:BANDWIDTH=3289288,AVERAGE-BANDWIDTH=3240596,CODECS="avc1.640020,mp4a.40.2",RESOLUTION=1280x720,FRAME-RATE=60.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 18 | v6/prog_index.m3u8 19 | #EXT-X-STREAM-INF:BANDWIDTH=1296989,AVERAGE-BANDWIDTH=1292926,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=768x432,FRAME-RATE=30.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 20 | v4/prog_index.m3u8 21 | #EXT-X-STREAM-INF:BANDWIDTH=922242,AVERAGE-BANDWIDTH=914722,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,FRAME-RATE=30.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 22 | v3/prog_index.m3u8 23 | #EXT-X-STREAM-INF:BANDWIDTH=553010,AVERAGE-BANDWIDTH=541239,CODECS="avc1.640015,mp4a.40.2",RESOLUTION=480x270,FRAME-RATE=30.000,AUDIO="aud1",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 24 | v2/prog_index.m3u8 25 | 26 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud2",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="6",URI="a2/prog_index.m3u8" 27 | 28 | #EXT-X-STREAM-INF:BANDWIDTH=2448841,AVERAGE-BANDWIDTH=2439704,CODECS="avc1.640020,ac-3",RESOLUTION=960x540,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 29 | v5/prog_index.m3u8 30 | #EXT-X-STREAM-INF:BANDWIDTH=8399417,AVERAGE-BANDWIDTH=8366033,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 31 | v9/prog_index.m3u8 32 | #EXT-X-STREAM-INF:BANDWIDTH=6674579,AVERAGE-BANDWIDTH=6528521,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 33 | v8/prog_index.m3u8 34 | #EXT-X-STREAM-INF:BANDWIDTH=5275609,AVERAGE-BANDWIDTH=4996715,CODECS="avc1.64002a,ac-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 35 | v7/prog_index.m3u8 36 | #EXT-X-STREAM-INF:BANDWIDTH=3510665,AVERAGE-BANDWIDTH=3461973,CODECS="avc1.640020,ac-3",RESOLUTION=1280x720,FRAME-RATE=60.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 37 | v6/prog_index.m3u8 38 | #EXT-X-STREAM-INF:BANDWIDTH=1518366,AVERAGE-BANDWIDTH=1514303,CODECS="avc1.64001e,ac-3",RESOLUTION=768x432,FRAME-RATE=30.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 39 | v4/prog_index.m3u8 40 | #EXT-X-STREAM-INF:BANDWIDTH=1143619,AVERAGE-BANDWIDTH=1136099,CODECS="avc1.64001e,ac-3",RESOLUTION=640x360,FRAME-RATE=30.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 41 | v3/prog_index.m3u8 42 | #EXT-X-STREAM-INF:BANDWIDTH=774387,AVERAGE-BANDWIDTH=762616,CODECS="avc1.640015,ac-3",RESOLUTION=480x270,FRAME-RATE=30.000,AUDIO="aud2",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 43 | v2/prog_index.m3u8 44 | 45 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="aud3",NAME="English",DEFAULT=YES,AUTOSELECT=YES,LANGUAGE="en",CHANNELS="6",URI="a3/prog_index.m3u8" 46 | 47 | #EXT-X-STREAM-INF:BANDWIDTH=2256841,AVERAGE-BANDWIDTH=2247704,CODECS="avc1.640020,ec-3",RESOLUTION=960x540,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 48 | v5/prog_index.m3u8 49 | #EXT-X-STREAM-INF:BANDWIDTH=8207417,AVERAGE-BANDWIDTH=8174033,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 50 | v9/prog_index.m3u8 51 | #EXT-X-STREAM-INF:BANDWIDTH=6482579,AVERAGE-BANDWIDTH=6336521,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 52 | v8/prog_index.m3u8 53 | #EXT-X-STREAM-INF:BANDWIDTH=5083609,AVERAGE-BANDWIDTH=4804715,CODECS="avc1.64002a,ec-3",RESOLUTION=1920x1080,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 54 | v7/prog_index.m3u8 55 | #EXT-X-STREAM-INF:BANDWIDTH=3318665,AVERAGE-BANDWIDTH=3269973,CODECS="avc1.640020,ec-3",RESOLUTION=1280x720,FRAME-RATE=60.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 56 | v6/prog_index.m3u8 57 | #EXT-X-STREAM-INF:BANDWIDTH=1326366,AVERAGE-BANDWIDTH=1322303,CODECS="avc1.64001e,ec-3",RESOLUTION=768x432,FRAME-RATE=30.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 58 | v4/prog_index.m3u8 59 | #EXT-X-STREAM-INF:BANDWIDTH=951619,AVERAGE-BANDWIDTH=944099,CODECS="avc1.64001e,ec-3",RESOLUTION=640x360,FRAME-RATE=30.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 60 | v3/prog_index.m3u8 61 | #EXT-X-STREAM-INF:BANDWIDTH=582387,AVERAGE-BANDWIDTH=570616,CODECS="avc1.640015,ec-3",RESOLUTION=480x270,FRAME-RATE=30.000,AUDIO="aud3",SUBTITLES="sub1",CLOSED-CAPTIONS="cc1" 62 | v2/prog_index.m3u8 63 | 64 | 65 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=186522,AVERAGE-BANDWIDTH=182077,URI="v7/iframe_index.m3u8",CODECS="avc1.64002a",RESOLUTION=1920x1080 66 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=133856,AVERAGE-BANDWIDTH=129936,URI="v6/iframe_index.m3u8",CODECS="avc1.640020",RESOLUTION=1280x720 67 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=98136,AVERAGE-BANDWIDTH=94286,URI="v5/iframe_index.m3u8",CODECS="avc1.640020",RESOLUTION=960x540 68 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=76704,AVERAGE-BANDWIDTH=74767,URI="v4/iframe_index.m3u8",CODECS="avc1.64001e",RESOLUTION=768x432 69 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=64078,AVERAGE-BANDWIDTH=62251,URI="v3/iframe_index.m3u8",CODECS="avc1.64001e",RESOLUTION=640x360 70 | #EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=38728,AVERAGE-BANDWIDTH=37866,URI="v2/iframe_index.m3u8",CODECS="avc1.640015",RESOLUTION=480x270 71 | -------------------------------------------------------------------------------- /test/spec/4_Playlists/4.3_Playlist-Tags/4.3.4_Master-Playlist-Tags/4.3.4.1_EXT-X-MEDIA.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const HLS = require('../../../../..'); 3 | const utils = require('../../../../helpers/utils'); 4 | 5 | // TYPE attribute is REQUIRED. 6 | test('#EXT-X-MEDIA_01', t => { 7 | utils.parseFail(t, ` 8 | #EXTM3U 9 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="audio" 10 | /video/main.m3u8 11 | #EXT-X-MEDIA:GROUP-ID="audio",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" 12 | `); 13 | utils.bothPass(t, ` 14 | #EXTM3U 15 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="audio" 16 | /video/main.m3u8 17 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="audio",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" 18 | `); 19 | }); 20 | 21 | // TYPE attribute: valid strings are AUDIO, VIDEO, 22 | // SUBTITLES and CLOSED-CAPTIONS. 23 | test('#EXT-X-MEDIA_02', t => { 24 | let playlist = HLS.parse(` 25 | #EXTM3U 26 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 27 | /video/main.m3u8 28 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" 29 | `); 30 | t.is(playlist.variants[0].audio[0].type, 'AUDIO'); 31 | playlist = HLS.parse(` 32 | #EXTM3U 33 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,VIDEO="test" 34 | /video/main.m3u8 35 | #EXT-X-MEDIA:TYPE=VIDEO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/video/en.m3u8" 36 | `); 37 | t.is(playlist.variants[0].video[0].type, 'VIDEO'); 38 | playlist = HLS.parse(` 39 | #EXTM3U 40 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES="test" 41 | /video/main.m3u8 42 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/subtitles/en.m3u8" 43 | `); 44 | t.is(playlist.variants[0].subtitles[0].type, 'SUBTITLES'); 45 | playlist = HLS.parse(` 46 | #EXTM3U 47 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" 48 | /video/main.m3u8 49 | #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1" 50 | `); 51 | t.is(playlist.variants[0].closedCaptions[0].type, 'CLOSED-CAPTIONS'); 52 | }); 53 | 54 | // If the TYPE is CLOSED-CAPTIONS, the URI 55 | // attribute MUST NOT be present. 56 | test('#EXT-X-MEDIA_03', t => { 57 | utils.parseFail(t, ` 58 | #EXTM3U 59 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" 60 | /video/main.m3u8 61 | #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1",URI="/audio/en.m3u8" 62 | `); 63 | utils.bothPass(t, ` 64 | #EXTM3U 65 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" 66 | /video/main.m3u8 67 | #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1" 68 | `); 69 | }); 70 | 71 | // GROUP-ID attribute is REQUIRED. 72 | test('#EXT-X-MEDIA_04', t => { 73 | utils.parseFail(t, ` 74 | #EXTM3U 75 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 76 | /video/main.m3u8 77 | #EXT-X-MEDIA:TYPE=AUDIO,NAME="en",DEFAULT=YES" 78 | `); 79 | utils.bothPass(t, ` 80 | #EXTM3U 81 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 82 | /video/main.m3u8 83 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES" 84 | `); 85 | }); 86 | 87 | // NAME attribute is REQUIRED. 88 | test('#EXT-X-MEDIA_05', t => { 89 | utils.parseFail(t, ` 90 | #EXTM3U 91 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 92 | /video/main.m3u8 93 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",DEFAULT=YES" 94 | `); 95 | utils.bothPass(t, ` 96 | #EXTM3U 97 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 98 | /video/main.m3u8 99 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES" 100 | `); 101 | }); 102 | 103 | // The FORCED attribute MUST NOT be present unless the 104 | // TYPE is SUBTITLES. 105 | test('#EXT-X-MEDIA_06', t => { 106 | utils.parseFail(t, ` 107 | #EXTM3U 108 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 109 | /video/main.m3u8 110 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,FORCED=YES 111 | `); 112 | utils.bothPass(t, ` 113 | #EXTM3U 114 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,SUBTITLES="test" 115 | /video/main.m3u8 116 | #EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/subtitles/en.m3u8",FORCED=YES 117 | `); 118 | }); 119 | 120 | // INSTREAM-ID attribute is REQUIRED if the TYPE attribute is CLOSED-CAPTIONS 121 | test('#EXT-X-MEDIA_07', t => { 122 | utils.parseFail(t, ` 123 | #EXTM3U 124 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" 125 | /video/main.m3u8 126 | #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES 127 | `); 128 | utils.bothPass(t, ` 129 | #EXTM3U 130 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,CLOSED-CAPTIONS="test" 131 | /video/main.m3u8 132 | #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="test",NAME="en",DEFAULT=YES,INSTREAM-ID="CC1" 133 | `); 134 | }); 135 | 136 | // All EXT-X-MEDIA tags in the same Group MUST have different NAME attributes. 137 | test('#EXT-X-MEDIA_08', t => { 138 | utils.parseFail(t, ` 139 | #EXTM3U 140 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 141 | /video/main.m3u8 142 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" 143 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" 144 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" 145 | `); 146 | utils.bothPass(t, ` 147 | #EXTM3U 148 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 149 | /video/main.m3u8 150 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" 151 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" 152 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="fr",URI="/audio/fr.m3u8" 153 | `); 154 | utils.bothPass(t, ` 155 | #EXTM3U 156 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 157 | /video/main.m3u8 158 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" 159 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" 160 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test2",NAME="ja",URI="/audio/ja.m3u8" 161 | `); 162 | }); 163 | 164 | // A Group MUST NOT have more than one member with a DEFAULT attribute of YES. 165 | test('#EXT-X-MEDIA_09', t => { 166 | utils.parseFail(t, ` 167 | #EXTM3U 168 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 169 | /video/main.m3u8 170 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" 171 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" 172 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="fr",DEFAULT=YES,URI="/audio/fr.m3u8" 173 | `); 174 | utils.bothPass(t, ` 175 | #EXTM3U 176 | #EXT-X-STREAM-INF:BANDWIDTH=1280000,AUDIO="test" 177 | /video/main.m3u8 178 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="en",DEFAULT=YES,URI="/audio/en.m3u8" 179 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="ja",URI="/audio/ja.m3u8" 180 | #EXT-X-MEDIA:TYPE=AUDIO,GROUP-ID="test",NAME="fr",URI="/audio/fr.m3u8" 181 | `); 182 | }); 183 | -------------------------------------------------------------------------------- /test/spec/Apple-Low-Latency/New_Media_Playlist_Tags_for_Low-Latency_HLS/04_EXT-X-PRELOAD-HINT.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../../helpers/utils'); 3 | const HLS = require('../../../..'); 4 | 5 | // TYPE=: (mandatory) 6 | test('#EXT-X-PRELOAD-HINT_01', t => { 7 | utils.bothPass(t, ` 8 | #EXTM3U 9 | #EXT-X-TARGETDURATION:2 10 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 11 | #EXT-X-PART-INF:PART-TARGET=0.2 12 | #EXTINF:2, 13 | fs240.mp4 14 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 15 | `); 16 | utils.parseFail(t, ` 17 | #EXTM3U 18 | #EXT-X-TARGETDURATION:2 19 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 20 | #EXT-X-PART-INF:PART-TARGET=0.2 21 | #EXTINF:2, 22 | fs240.mp4 23 | #EXT-X-PRELOAD-HINT:URI="fs241.mp4",BYTERANGE-LENGTH=20000 24 | `); 25 | }); 26 | 27 | // If hint-type is PART, the resource is an upcoming Partial Segment. 28 | test('#EXT-X-PRELOAD-HINT_02', t => { 29 | const {segments} = HLS.parse(` 30 | #EXTM3U 31 | #EXT-X-TARGETDURATION:2 32 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 33 | #EXT-X-PART-INF:PART-TARGET=0.2 34 | #EXTINF:2, 35 | fs240.mp4 36 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 37 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 38 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 39 | `); 40 | 41 | t.is(segments.length, 2); 42 | const {parts} = segments[1]; 43 | t.is(parts.length, 3); 44 | let offset = 0; 45 | const length = 20000; 46 | for (const [index, part] of parts.entries()) { 47 | t.is(part.uri, 'fs241.mp4'); 48 | t.deepEqual(part.byterange, {offset, length}); 49 | offset += length; 50 | if (index === 2) { 51 | t.true(part.hint); 52 | } else { 53 | t.false(part.hint); 54 | } 55 | } 56 | }); 57 | 58 | // If hint-type is MAP, the resource is an upcoming Media Initialization Section. 59 | test('#EXT-X-PRELOAD-HINT_03', t => { 60 | const {segments} = HLS.parse(` 61 | #EXTM3U 62 | #EXT-X-VERSION:6 63 | #EXT-X-TARGETDURATION:2 64 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 65 | #EXT-X-PART-INF:PART-TARGET=0.2 66 | #EXT-X-MAP:URI="map-0" 67 | #EXTINF:2, 68 | fs240.mp4 69 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 70 | #EXT-X-PRELOAD-HINT:TYPE=MAP,URI="map-1" 71 | 72 | `); 73 | 74 | t.is(segments.length, 2); 75 | for (const [index, {map}] of segments.entries()) { 76 | t.is(map.uri, `map-${index}`); 77 | } 78 | }); 79 | 80 | // URI=: (mandatory) 81 | test('#EXT-X-PRELOAD-HINT_04', t => { 82 | utils.bothPass(t, ` 83 | #EXTM3U 84 | #EXT-X-TARGETDURATION:2 85 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 86 | #EXT-X-PART-INF:PART-TARGET=0.2 87 | #EXTINF:2, 88 | fs240.mp4 89 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4" 90 | `); 91 | utils.parseFail(t, ` 92 | #EXTM3U 93 | #EXT-X-TARGETDURATION:2 94 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 95 | #EXT-X-PART-INF:PART-TARGET=0.2 96 | #EXTINF:2, 97 | fs240.mp4 98 | #EXT-X-PRELOAD-HINT:TYPE=PART 99 | `); 100 | }); 101 | 102 | // BYTERANGE-START=: ... Its absence implies a value of 0. 103 | test('#EXT-X-PRELOAD-HINT_05', t => { 104 | const {segments} = HLS.parse(` 105 | #EXTM3U 106 | #EXT-X-TARGETDURATION:2 107 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 108 | #EXT-X-PART-INF:PART-TARGET=0.2 109 | #EXTINF:2, 110 | fs240.mp4 111 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 112 | `); 113 | 114 | const {parts} = segments[1]; 115 | t.is(parts[0].byterange.offset, 0); 116 | }); 117 | 118 | // If the Playlist contains EXT-X-PART tags and does not contain an EXT-X-ENDLIST tag, 119 | // the Playlist must contain an EXT-X-PRELOAD-HINT tag with a TYPE=PART attribute 120 | // to hint the URI of the next EXT-X-PART tag that is expected to be added to the 121 | // Playlist (and its byte range, if applicable). 122 | test('#EXT-X-PRELOAD-HINT_06', t => { 123 | utils.bothPass(t, ` 124 | #EXTM3U 125 | #EXT-X-TARGETDURATION:2 126 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 127 | #EXT-X-PART-INF:PART-TARGET=0.2 128 | #EXTINF:2, 129 | fs240.mp4 130 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 131 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 132 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=40000,BYTERANGE-LENGTH=20000 133 | `); 134 | utils.parseFail(t, ` 135 | #EXTM3U 136 | #EXT-X-TARGETDURATION:2 137 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 138 | #EXT-X-PART-INF:PART-TARGET=0.2 139 | #EXTINF:2, 140 | fs240.mp4 141 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 142 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 143 | `); 144 | utils.bothPass(t, ` 145 | #EXTM3U 146 | #EXT-X-TARGETDURATION:2 147 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 148 | #EXT-X-PART-INF:PART-TARGET=0.2 149 | #EXTINF:2, 150 | fs240.mp4 151 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@0 152 | #EXT-X-PART:DURATION=0.2,URI="fs241.mp4",BYTERANGE=20000@20000 153 | #EXT-X-ENDLIST 154 | `); 155 | }); 156 | 157 | // Servers should not add more than one EXT-X-PRELOAD-HINT 158 | // tag with the same TYPE attribute to a Playlist. 159 | test('#EXT-X-PRELOAD-HINT_07', t => { 160 | utils.bothPass(t, ` 161 | #EXTM3U 162 | #EXT-X-TARGETDURATION:2 163 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 164 | #EXT-X-PART-INF:PART-TARGET=0.2 165 | #EXTINF:2, 166 | fs240.mp4 167 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 168 | `); 169 | utils.parseFail(t, ` 170 | #EXTM3U 171 | #EXT-X-TARGETDURATION:2 172 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 173 | #EXT-X-PART-INF:PART-TARGET=0.2 174 | #EXTINF:2, 175 | fs240.mp4 176 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 177 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-START=20000,BYTERANGE-LENGTH=20000 178 | `); 179 | utils.bothPass(t, ` 180 | #EXTM3U 181 | #EXT-X-VERSION:6 182 | #EXT-X-TARGETDURATION:2 183 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 184 | #EXT-X-PART-INF:PART-TARGET=0.2 185 | #EXT-X-MAP:URI="map-0" 186 | #EXTINF:2, 187 | fs240.mp4 188 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 189 | #EXT-X-PRELOAD-HINT:TYPE=MAP,URI="map-1" 190 | `); 191 | utils.parseFail(t, ` 192 | #EXTM3U 193 | #EXT-X-VERSION:6 194 | #EXT-X-TARGETDURATION:2 195 | #EXT-X-SERVER-CONTROL:CAN-BLOCK-RELOAD=YES,CAN-SKIP-UNTIL=12.0,HOLD-BACK=6.0,PART-HOLD-BACK=0.2 196 | #EXT-X-PART-INF:PART-TARGET=0.2 197 | #EXT-X-MAP:URI="map-0" 198 | #EXTINF:2, 199 | fs240.mp4 200 | #EXT-X-PRELOAD-HINT:TYPE=PART,URI="fs241.mp4",BYTERANGE-LENGTH=20000 201 | #EXT-X-PRELOAD-HINT:TYPE=MAP,URI="map-1" 202 | #EXT-X-PRELOAD-HINT:TYPE=MAP,URI="map-2" 203 | `); 204 | }); 205 | -------------------------------------------------------------------------------- /test/spec/7_Protocol-version-compatibility/7_EXT-X-VERSION.spec.js: -------------------------------------------------------------------------------- 1 | const test = require('ava'); 2 | const utils = require('../../helpers/utils'); 3 | 4 | // A Playlist that contains tags or attributes that are not compatible 5 | // with protocol version 1 MUST include an EXT-X-VERSION tag. 6 | test('#EXT-X-VERSION_02', t => { 7 | utils.bothPass(t, ` 8 | #EXTM3U 9 | #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com" 10 | `); 11 | utils.parseFail(t, ` 12 | #EXTM3U 13 | #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 14 | `); 15 | utils.bothPass(t, ` 16 | #EXTM3U 17 | #EXT-X-VERSION:2 18 | #EXT-X-SESSION-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 19 | `); 20 | }); 21 | 22 | // A Media Playlist MUST indicate a EXT-X-VERSION of 2 or higher if it 23 | // contains: 24 | // - The IV attribute of the EXT-X-KEY tag. 25 | test('#EXT-X-VERSION_03', t => { 26 | utils.bothPass(t, ` 27 | #EXTM3U 28 | #EXT-X-VERSION:1 29 | #EXT-X-TARGETDURATION:10 30 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" 31 | #EXTINF:10, 32 | http://example.com 33 | `); 34 | utils.parseFail(t, ` 35 | #EXTM3U 36 | #EXT-X-VERSION:1 37 | #EXT-X-TARGETDURATION:10 38 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 39 | #EXTINF:10, 40 | http://example.com 41 | `); 42 | utils.bothPass(t, ` 43 | #EXTM3U 44 | #EXT-X-VERSION:2 45 | #EXT-X-TARGETDURATION:10 46 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",IV=0xFFEEDDCCBBAA99887766554433221100 47 | #EXTINF:10, 48 | http://example.com 49 | `); 50 | }); 51 | 52 | // A Media Playlist MUST indicate a EXT-X-VERSION of 3 or higher if it 53 | // contains: 54 | // - Floating-point EXTINF duration values. 55 | test('#EXT-X-VERSION_04', t => { 56 | utils.bothPass(t, ` 57 | #EXTM3U 58 | #EXT-X-VERSION:2 59 | #EXT-X-TARGETDURATION:10 60 | #EXTINF:10, 61 | http://example.com 62 | `); 63 | utils.parseFail(t, ` 64 | #EXTM3U 65 | #EXT-X-VERSION:2 66 | #EXT-X-TARGETDURATION:10 67 | #EXTINF:9.9, 68 | http://example.com 69 | `); 70 | utils.bothPass(t, ` 71 | #EXTM3U 72 | #EXT-X-VERSION:3 73 | #EXT-X-TARGETDURATION:10 74 | #EXTINF:9.9, 75 | http://example.com 76 | `); 77 | }); 78 | 79 | // A Media Playlist MUST indicate a EXT-X-VERSION of 4 or higher if it 80 | // contains: 81 | // - The EXT-X-BYTERANGE tag. 82 | test('#EXT-X-VERSION_05', t => { 83 | utils.bothPass(t, ` 84 | #EXTM3U 85 | #EXT-X-VERSION:3 86 | #EXT-X-TARGETDURATION:10 87 | #EXTINF:10, 88 | http://example.com 89 | `); 90 | utils.parseFail(t, ` 91 | #EXTM3U 92 | #EXT-X-VERSION:3 93 | #EXT-X-TARGETDURATION:10 94 | #EXT-X-BYTERANGE:256@100 95 | #EXTINF:10, 96 | http://example.com 97 | `); 98 | utils.bothPass(t, ` 99 | #EXTM3U 100 | #EXT-X-VERSION:4 101 | #EXT-X-TARGETDURATION:10 102 | #EXT-X-BYTERANGE:256@100 103 | #EXTINF:10, 104 | http://example.com 105 | `); 106 | }); 107 | 108 | // A Media Playlist MUST indicate a EXT-X-VERSION of 4 or higher if it 109 | // contains: 110 | // - The EXT-X-I-FRAMES-ONLY tag. 111 | test('#EXT-X-VERSION_06', t => { 112 | utils.bothPass(t, ` 113 | #EXTM3U 114 | #EXT-X-VERSION:3 115 | #EXT-X-TARGETDURATION:10 116 | `); 117 | utils.parseFail(t, ` 118 | #EXTM3U 119 | #EXT-X-VERSION:3 120 | #EXT-X-TARGETDURATION:10 121 | #EXT-X-I-FRAMES-ONLY 122 | `); 123 | utils.bothPass(t, ` 124 | #EXTM3U 125 | #EXT-X-VERSION:4 126 | #EXT-X-TARGETDURATION:10 127 | #EXT-X-I-FRAMES-ONLY 128 | `); 129 | }); 130 | 131 | // A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it 132 | // contains: 133 | // - The KEYFORMAT attributes of the EXT-X-KEY tag. 134 | test('#EXT-X-VERSION_07', t => { 135 | utils.bothPass(t, ` 136 | #EXTM3U 137 | #EXT-X-VERSION:4 138 | #EXT-X-TARGETDURATION:10 139 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" 140 | #EXTINF:10, 141 | http://example.com 142 | `); 143 | utils.parseFail(t, ` 144 | #EXTM3U 145 | #EXT-X-VERSION:4 146 | #EXT-X-TARGETDURATION:10 147 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",KEYFORMAT="identity" 148 | #EXTINF:10, 149 | http://example.com 150 | `); 151 | utils.bothPass(t, ` 152 | #EXTM3U 153 | #EXT-X-VERSION:5 154 | #EXT-X-TARGETDURATION:10 155 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",KEYFORMAT="identity" 156 | #EXTINF:10, 157 | http://example.com 158 | `); 159 | }); 160 | 161 | // A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it 162 | // contains: 163 | // - The KEYFORMATVERSIONS attributes of the EXT-X-KEY tag. 164 | test('#EXT-X-VERSION_08', t => { 165 | utils.bothPass(t, ` 166 | #EXTM3U 167 | #EXT-X-VERSION:4 168 | #EXT-X-TARGETDURATION:10 169 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" 170 | #EXTINF:10, 171 | http://example.com 172 | `); 173 | utils.parseFail(t, ` 174 | #EXTM3U 175 | #EXT-X-VERSION:4 176 | #EXT-X-TARGETDURATION:10 177 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",KEYFORMATVERSIONS="1" 178 | #EXTINF:10, 179 | http://example.com 180 | `); 181 | utils.bothPass(t, ` 182 | #EXTM3U 183 | #EXT-X-VERSION:5 184 | #EXT-X-TARGETDURATION:10 185 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com",KEYFORMATVERSIONS="1" 186 | #EXTINF:10, 187 | http://example.com `); 188 | }); 189 | 190 | // A Media Playlist MUST indicate a EXT-X-VERSION of 5 or higher if it 191 | // contains: 192 | // - The EXT-X-MAP tag. 193 | test('#EXT-X-VERSION_09', t => { 194 | utils.bothPass(t, ` 195 | #EXTM3U 196 | #EXT-X-VERSION:4 197 | #EXT-X-TARGETDURATION:10 198 | #EXT-X-I-FRAMES-ONLY 199 | `); 200 | utils.parseFail(t, ` 201 | #EXTM3U 202 | #EXT-X-VERSION:4 203 | #EXT-X-TARGETDURATION:10 204 | #EXT-X-I-FRAMES-ONLY 205 | #EXT-X-MAP:URI="http://example.com" 206 | #EXTINF:10, 207 | http://example.com 208 | `); 209 | utils.bothPass(t, ` 210 | #EXTM3U 211 | #EXT-X-VERSION:5 212 | #EXT-X-TARGETDURATION:10 213 | #EXT-X-I-FRAMES-ONLY 214 | #EXT-X-MAP:URI="http://example.com" 215 | #EXTINF:10, 216 | http://example.com 217 | `); 218 | }); 219 | 220 | // A Media Playlist MUST indicate a EXT-X-VERSION of 6 or higher if it 221 | // contains: 222 | // - The EXT-X-MAP tag in a Media Playlist that does not contain EXT-X-I-FRAMES-ONLY. 223 | test('#EXT-X-VERSION_10', t => { 224 | utils.bothPass(t, ` 225 | #EXTM3U 226 | #EXT-X-VERSION:5 227 | #EXT-X-TARGETDURATION:10 228 | #EXT-X-I-FRAMES-ONLY 229 | #EXT-X-MAP:URI="http://example.com" 230 | #EXTINF:10, 231 | http://example.com 232 | `); 233 | utils.parseFail(t, ` 234 | #EXTM3U 235 | #EXT-X-VERSION:5 236 | #EXT-X-TARGETDURATION:10 237 | #EXT-X-MAP:URI="http://example.com" 238 | #EXTINF:10, 239 | http://example.com 240 | `); 241 | utils.bothPass(t, ` 242 | #EXTM3U 243 | #EXT-X-VERSION:6 244 | #EXT-X-TARGETDURATION:10 245 | #EXT-X-MAP:URI="http://example.com" 246 | #EXTINF:10, 247 | http://example.com 248 | `); 249 | }); 250 | 251 | // A Master Playlist MUST indicate a EXT-X-VERSION of 7 or higher if it 252 | // contains: 253 | // - "SERVICE" values for the INSTREAM-ID attribute of the EXT-X-MEDIA tag. 254 | test('#EXT-X-VERSION_11', t => { 255 | utils.bothPass(t, ` 256 | #EXTM3U 257 | #EXT-X-VERSION:6 258 | #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",NAME="JP",INSTREAM-ID="CC1" 259 | #EXT-X-STREAM-INF:BANDWIDTH=500000,CLOSED-CAPTIONS="cc" 260 | http://example.com 261 | `); 262 | utils.parseFail(t, ` 263 | #EXTM3U 264 | #EXT-X-VERSION:6 265 | #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",NAME="JP",INSTREAM-ID="SERVICE1" 266 | #EXT-X-STREAM-INF:BANDWIDTH=500000,CLOSED-CAPTIONS="cc" 267 | http://example.com 268 | `); 269 | utils.bothPass(t, ` 270 | #EXTM3U 271 | #EXT-X-VERSION:7 272 | #EXT-X-MEDIA:TYPE=CLOSED-CAPTIONS,GROUP-ID="cc",NAME="JP",INSTREAM-ID="SERVICE1" 273 | #EXT-X-STREAM-INF:BANDWIDTH=500000,CLOSED-CAPTIONS="cc" 274 | http://example.com 275 | `); 276 | }); 277 | 278 | // A Media Playlist MUST indicate a EXT-X-VERSION of 8 or higher if it 279 | // contains: 280 | // - the "EXT-X-GAP" tag. 281 | test('#EXT-X-VERSION_12', t => { 282 | utils.parseFail(t, ` 283 | #EXTM3U 284 | #EXT-X-VERSION:1 285 | #EXTINF:5 286 | #EXT-X-GAP 287 | http://example.com 288 | `); 289 | }); 290 | -------------------------------------------------------------------------------- /test/spec/HLSJS-LHLS/01_EXT-X-PREFETCH.spec.js: -------------------------------------------------------------------------------- 1 | const test = require("ava"); 2 | const utils = require("../../helpers/utils"); 3 | const HLS = require("../../.."); 4 | 5 | test("#EXT-X-PREFETCH_01", t => { 6 | utils.bothPass( 7 | t, 8 | ` 9 | #EXTM3U 10 | #EXT-X-VERSION:3 11 | #EXT-X-TARGETDURATION:2 12 | #EXT-X-MEDIA-SEQUENCE: 0 13 | #EXT-X-DISCONTINUITY-SEQUENCE: 0 14 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z 15 | #EXTINF:2.000 16 | https://foo.com/bar/0.ts 17 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z 18 | #EXTINF:2.000 19 | https://foo.com/bar/1.ts 20 | 21 | #EXT-X-PREFETCH:https://foo.com/bar/2.ts 22 | #EXT-X-PREFETCH:https://foo.com/bar/3.ts 23 | ` 24 | ); 25 | }); 26 | 27 | test("#EXT-X-PREFETCH_02", t => { 28 | const parsed = HLS.parse(` 29 | #EXTM3U 30 | #EXT-X-VERSION:3 31 | #EXT-X-TARGETDURATION:2 32 | #EXT-X-MEDIA-SEQUENCE: 0 33 | #EXT-X-DISCONTINUITY-SEQUENCE: 0 34 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z 35 | #EXTINF:2.000 36 | https://foo.com/bar/0.ts 37 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z 38 | #EXTINF:2.000 39 | https://foo.com/bar/1.ts 40 | 41 | #EXT-X-PREFETCH:https://foo.com/bar/2.ts 42 | #EXT-X-PREFETCH:https://foo.com/bar/3.ts 43 | `); 44 | const {prefetchSegments} = parsed; 45 | 46 | t.is(prefetchSegments.length, 2); 47 | t.is(prefetchSegments[0].uri, "https://foo.com/bar/2.ts"); 48 | t.is(prefetchSegments[1].uri, "https://foo.com/bar/3.ts"); 49 | 50 | const stringified = HLS.stringify(parsed); 51 | 52 | t.true(stringified.includes('#EXT-X-PREFETCH:https://foo.com/bar/2.ts')); 53 | t.true(stringified.includes('#EXT-X-PREFETCH:https://foo.com/bar/3.ts')); 54 | }); 55 | 56 | // If delivering a low-latency stream, the server must deliver at least one 57 | // prefetch segment, but no more than two. 58 | test("#EXT-X-PREFETCH_03", t => { 59 | const parsed = HLS.parse(` 60 | #EXTM3U 61 | #EXT-X-VERSION:3 62 | #EXT-X-TARGETDURATION:2 63 | #EXT-X-MEDIA-SEQUENCE: 0 64 | #EXT-X-DISCONTINUITY-SEQUENCE: 0 65 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z 66 | #EXTINF:2.000 67 | https://foo.com/bar/0.ts 68 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z 69 | #EXTINF:2.000 70 | https://foo.com/bar/1.ts 71 | 72 | #EXT-X-PREFETCH:https://foo.com/bar/2.ts 73 | #EXT-X-PREFETCH:https://foo.com/bar/3.ts 74 | #EXT-X-PREFETCH:https://foo.com/bar/4.ts 75 | `); 76 | const {prefetchSegments} = parsed; 77 | t.is(prefetchSegments.length, 3); 78 | 79 | try { 80 | HLS.stringify(parsed); 81 | t.fail('The server must deliver no more than two prefetch segments'); 82 | } catch { 83 | t.pass(); 84 | } 85 | }); 86 | 87 | // These segments must appear after all complete segments. 88 | test("#EXT-X-PREFETCH_04", t => { 89 | try { 90 | HLS.parse(` 91 | #EXTM3U 92 | #EXT-X-VERSION:3 93 | #EXT-X-TARGETDURATION:2 94 | #EXT-X-MEDIA-SEQUENCE: 0 95 | #EXT-X-DISCONTINUITY-SEQUENCE: 0 96 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z 97 | #EXTINF:2.000 98 | https://foo.com/bar/0.ts 99 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z 100 | #EXTINF:2.000 101 | https://foo.com/bar/1.ts 102 | 103 | #EXT-X-PREFETCH:https://foo.com/bar/2.ts 104 | #EXT-X-PREFETCH:https://foo.com/bar/3.ts 105 | 106 | #EXTINF:2.000 107 | https://foo.com/bar/4.ts 108 | `); 109 | t.fail('Prefetch segments must appear after all complete segments'); 110 | } catch { 111 | t.pass(); 112 | } 113 | }); 114 | 115 | // A prefetch segment's Discontinuity Sequence Number is the value of the 116 | // EXT-X-DISCONTINUITY-SEQUENCE tag (or zero if none) plus the number of 117 | // EXT-X-DISCONTINUITY and EXT-X-PREFETCH-DISCONTINUITY tags in the Playlist 118 | // preceding the URI line of the segment. 119 | test("#EXT-X-PREFETCH_05", t => { 120 | const parsed = HLS.parse(` 121 | #EXTM3U 122 | #EXT-X-VERSION:3 123 | #EXT-X-TARGETDURATION:2 124 | #EXT-X-MEDIA-SEQUENCE: 0 125 | #EXT-X-DISCONTINUITY-SEQUENCE: 100 126 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:06.531Z 127 | #EXTINF:2.000 128 | https://foo.com/bar/0.ts 129 | #EXT-X-PROGRAM-DATE-TIME:2018-09-05T20:59:08.531Z 130 | #EXTINF:2.000 131 | https://foo.com/bar/1.ts 132 | #EXT-X-DISCONTINUITY 133 | #EXTINF:2.000 134 | https://foo.com/bar/1.ts 135 | 136 | #EXT-X-PREFETCH-DISCONTINUITY 137 | #EXT-X-PREFETCH:https://foo.com/bar/5.ts 138 | #EXT-X-PREFETCH:https://foo.com/bar/6.ts 139 | `); 140 | const {prefetchSegments} = parsed; 141 | t.is(prefetchSegments[1].discontinuitySequence, 102); 142 | }); 143 | 144 | // If a prefetch segment is the first segment in a manifest, its Media Sequence 145 | // Number is either 0, or declared in the Playlist. 146 | // The Media Sequence Number of every other prefetch segment is equal to the 147 | // Media Sequence Number of the complete segment or prefetch segment that 148 | // precedes it plus one. 149 | test("#EXT-X-PREFETCH_06", t => { 150 | let parsed; 151 | parsed = HLS.parse(` 152 | #EXTM3U 153 | #EXT-X-VERSION:3 154 | #EXT-X-TARGETDURATION:2 155 | 156 | #EXT-X-PREFETCH:https://foo.com/bar/5.ts 157 | #EXT-X-PREFETCH:https://foo.com/bar/6.ts 158 | `); 159 | t.is(parsed.prefetchSegments[0].mediaSequenceNumber, 0); 160 | t.is(parsed.prefetchSegments[1].mediaSequenceNumber, 1); 161 | 162 | parsed = HLS.parse(` 163 | #EXTM3U 164 | #EXT-X-VERSION:3 165 | #EXT-X-TARGETDURATION:2 166 | #EXT-X-MEDIA-SEQUENCE: 100 167 | 168 | #EXT-X-PREFETCH:https://foo.com/bar/5.ts 169 | #EXT-X-PREFETCH:https://foo.com/bar/6.ts 170 | `); 171 | t.is(parsed.prefetchSegments[0].mediaSequenceNumber, 100); 172 | t.is(parsed.prefetchSegments[1].mediaSequenceNumber, 101); 173 | }); 174 | 175 | // A prefetch segment must not be advertised with an EXTINF tag. The duration of 176 | // a prefetch segment must be equal to or less than what is specified by the 177 | // EXT-X-TARGETDURATION tag. 178 | test("#EXT-X-PREFETCH_07", t => { 179 | try { 180 | HLS.parse(` 181 | #EXTM3U 182 | #EXT-X-VERSION:3 183 | #EXT-X-TARGETDURATION:2 184 | 185 | #EXTINF:2.000 186 | #EXT-X-PREFETCH:https://foo.com/bar/5.ts 187 | #EXTINF:2.000 188 | #EXT-X-PREFETCH:https://foo.com/bar/6.ts 189 | `); 190 | t.fail('A prefetch segment must not be advertised with an EXTINF tag'); 191 | } catch { 192 | t.pass(); 193 | } 194 | }); 195 | 196 | // A prefetch segment must not be advertised with an EXT-X-DISCONTINUITY tag. 197 | // To insert a discontinuity just for prefetch segments, the server must insert 198 | // the EXT-X-PREFETCH-DISCONTINUITY tag before the newest EXT-X-PREFETCH tag of 199 | // the new discontinuous range. 200 | test("#EXT-X-PREFETCH_08", t => { 201 | try { 202 | HLS.parse(` 203 | #EXTM3U 204 | #EXT-X-VERSION:3 205 | #EXT-X-TARGETDURATION:2 206 | 207 | #EXT-X-DISCONTINUITY 208 | #EXT-X-PREFETCH:https://foo.com/bar/5.ts 209 | #EXT-X-PREFETCH:https://foo.com/bar/6.ts 210 | `); 211 | t.fail('A prefetch segment must not be advertised with an EXT-X-DISCONTINUITY tag'); 212 | } catch { 213 | t.pass(); 214 | } 215 | }); 216 | 217 | // Prefetch segments must not be advertised with an EXT-X-MAP tag. 218 | test("#EXT-X-PREFETCH_09", t => { 219 | try { 220 | HLS.parse(` 221 | #EXTM3U 222 | #EXT-X-VERSION:3 223 | #EXT-X-TARGETDURATION:2 224 | 225 | #EXT-X-MAP:URI="http://example.com/map-1" 226 | #EXT-X-PREFETCH:https://foo.com/bar/5.ts 227 | #EXT-X-PREFETCH:https://foo.com/bar/6.ts 228 | `); 229 | t.fail('Prefetch segments must not be advertised with an EXT-X-MAP tag'); 230 | } catch { 231 | t.pass(); 232 | } 233 | }); 234 | 235 | // Prefetch segments may be advertised with an EXT-X-KEY tag. The key itself 236 | // must be complete; the server must not expect the client to progressively stream keys. 237 | test("#EXT-X-PREFETCH_10", t => { 238 | const parsed = HLS.parse(` 239 | #EXTM3U 240 | #EXT-X-VERSION:3 241 | #EXT-X-TARGETDURATION:2 242 | 243 | #EXT-X-KEY:METHOD=AES-128,URI="http://example.com" 244 | #EXT-X-PREFETCH:https://foo.com/bar/5.ts 245 | #EXT-X-PREFETCH:https://foo.com/bar/6.ts 246 | `); 247 | const {prefetchSegments} = parsed; 248 | t.truthy(prefetchSegments[0].key); 249 | t.is(prefetchSegments[0].key.uri, 'http://example.com'); 250 | t.truthy(prefetchSegments[1].key); 251 | t.is(prefetchSegments[1].key.uri, 'http://example.com'); 252 | }); 253 | --------------------------------------------------------------------------------