Skip to content

Commit 4b61208

Browse files
authored
Use segment #EXT-X-BITRATE tag value where applicable (#6843)
Adds Fragment byteLength getter and bitrate getter/setter
1 parent 9c5089d commit 4b61208

File tree

7 files changed

+118
-17
lines changed

7 files changed

+118
-17
lines changed

README.md

+6-11
Original file line numberDiff line numberDiff line change
@@ -86,20 +86,18 @@ For details on the HLS format and these tags' meanings, see https://datatracker.
8686
- `#EXT-X-CONTENT-STEERING:<attribute-list>` Content Steering
8787
- `#EXT-X-DEFINE:<attribute-list>` Variable Substitution (`NAME,VALUE,QUERYPARAM` attributes)
8888

89-
The following properties are added to their respective variants' attribute list but are not implemented in their selection and playback.
90-
91-
- `VIDEO-RANGE` (See [#2489](https://github.com/video-dev/hls.js/issues/2489))
92-
9389
#### Media Playlist tags
9490

95-
- `#EXTM3U`
96-
- `#EXT-X-VERSION=<n>`
91+
- `#EXTM3U` (ignored)
92+
- `#EXT-X-INDEPENDENT-SEGMENTS` (ignored)
93+
- `#EXT-X-VERSION=<n>` (value is ignored)
9794
- `#EXTINF:<duration>,[<title>]`
9895
- `#EXT-X-ENDLIST`
9996
- `#EXT-X-MEDIA-SEQUENCE=<n>`
10097
- `#EXT-X-TARGETDURATION=<n>`
10198
- `#EXT-X-DISCONTINUITY`
10299
- `#EXT-X-DISCONTINUITY-SEQUENCE=<n>`
100+
- `#EXT-X-BITRATE`
103101
- `#EXT-X-BYTERANGE=<n>[@<o>]`
104102
- `#EXT-X-MAP:<attribute-list>`
105103
- `#EXT-X-KEY:<attribute-list>` (`KEYFORMAT="identity",METHOD=SAMPLE-AES` is only supports with MPEG-2 TS segments)
@@ -115,11 +113,7 @@ The following properties are added to their respective variants' attribute list
115113
- `#EXT-X-DEFINE:<attribute-list>` Variable Import and Substitution (`NAME,VALUE,IMPORT,QUERYPARAM` attributes)
116114
- `#EXT-X-GAP` (Skips loading GAP segments and parts. Skips playback of unbuffered program containing only GAP content and no suitable alternates. See [#2940](https://github.com/video-dev/hls.js/issues/2940))
117115

118-
The following tags are added to their respective fragment's attribute list but are not implemented in streaming and playback.
119-
120-
- `#EXT-X-BITRATE` (Not used in ABR controller)
121-
122-
Parsed but missing feature support
116+
Parsed but missing feature support:
123117

124118
- `#EXT-X-PRELOAD-HINT:<attribute-list>` (See [#5074](https://github.com/video-dev/hls.js/issues/3988))
125119
- #5074
@@ -129,6 +123,7 @@ Parsed but missing feature support
129123
For a complete list of issues, see ["Top priorities" in the Release Planning and Backlog project tab](https://github.com/video-dev/hls.js/projects/6). Codec support is dependent on the runtime environment (for example, not all browsers on the same OS support HEVC).
130124

131125
- `#EXT-X-I-FRAME-STREAM-INF` I-frame Media Playlist files
126+
- `REQ-VIDEO-LAYOUT` is not used in variant filtering or selection
132127
- "identity" format `SAMPLE-AES` method keys with fmp4, aac, mp3, vtt... segments (MPEG-2 TS only)
133128
- MPEG-2 TS segments with FairPlay Streaming, PlayReady, or Widevine encryption
134129
- FairPlay Streaming legacy keys (For com.apple.fps.1_0 use native Safari playback)

api-extractor/report/hls.js.api.md

+5
Original file line numberDiff line numberDiff line change
@@ -1716,8 +1716,13 @@ export class Fragment extends BaseSegment {
17161716
// (undocumented)
17171717
addStart(value: number): void;
17181718
// (undocumented)
1719+
get bitrate(): number | null;
1720+
set bitrate(value: number);
1721+
// (undocumented)
17191722
bitrateTest: boolean;
17201723
// (undocumented)
1724+
get byteLength(): number | null;
1725+
// (undocumented)
17211726
cc: number;
17221727
// (undocumented)
17231728
data?: Uint8Array;

src/controller/abr-controller.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -285,9 +285,10 @@ class AbrController extends Logger implements AbrComponentAPI {
285285
const bwEstimate: number = this.getBwEstimate();
286286
const levels = hls.levels;
287287
const level = levels[frag.level];
288-
const expectedLen =
289-
stats.total ||
290-
Math.max(stats.loaded, Math.round((duration * level.averageBitrate) / 8));
288+
const expectedLen = Math.max(
289+
stats.loaded,
290+
Math.round((duration * (frag.bitrate || level.averageBitrate)) / 8),
291+
);
291292
let timeStreaming = loadedFirstByte ? timeLoading - ttfb : timeLoading;
292293
if (timeStreaming < 1 && loadedFirstByte) {
293294
timeStreaming = Math.min(timeLoading, (stats.loaded * 8) / bwEstimate);
@@ -880,8 +881,8 @@ class AbrController extends Logger implements AbrComponentAPI {
880881
currentFragDuration &&
881882
bufferStarvationDelay >= currentFragDuration * 2 &&
882883
maxStarvationDelay === 0
883-
? levels[i].averageBitrate
884-
: levels[i].maxBitrate;
884+
? levelInfo.averageBitrate
885+
: levelInfo.maxBitrate;
885886
const fetchDuration: number = this.getTimeToLoadFrag(
886887
ttfbEstimateSec,
887888
adjustedbw,

src/loader/fragment-loader.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { ErrorDetails, ErrorTypes } from '../errors';
22
import { getLoaderConfigWithoutReties } from '../utils/error-helper';
3-
import type { HlsConfig } from '../config';
43
import type { BaseSegment, Fragment, Part } from './fragment';
4+
import type { HlsConfig } from '../config';
55
import type {
66
ErrorData,
77
FragLoadedData,

src/loader/fragment.ts

+33
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ export class Fragment extends BaseSegment {
159159
private _decryptdata: LevelKey | null = null;
160160
private _programDateTime: number | null = null;
161161
private _ref: MediaFragmentRef | null = null;
162+
// Approximate bit rate of the fragment expressed in bits per second (bps) as indicated by the last EXT-X-BITRATE (kbps) tag
163+
private _bitrate?: number;
162164

163165
public rawProgramDateTime: string | null = null;
164166
public tagList: Array<string[]> = [];
@@ -219,6 +221,37 @@ export class Fragment extends BaseSegment {
219221
this.type = type;
220222
}
221223

224+
get byteLength(): number | null {
225+
if (this.hasStats) {
226+
const total = this.stats.total;
227+
if (total) {
228+
return total;
229+
}
230+
}
231+
if (this.byteRange) {
232+
const start = this.byteRange[0];
233+
const end = this.byteRange[1];
234+
if (Number.isFinite(start) && Number.isFinite(end)) {
235+
return (end as number) - (start as number);
236+
}
237+
}
238+
return null;
239+
}
240+
241+
get bitrate(): number | null {
242+
if (this.byteLength) {
243+
return (this.byteLength * 8) / this.duration;
244+
}
245+
if (this._bitrate) {
246+
return this._bitrate;
247+
}
248+
return null;
249+
}
250+
251+
set bitrate(value: number) {
252+
this._bitrate = value;
253+
}
254+
222255
get decryptdata(): LevelKey | null {
223256
const { levelkeys } = this;
224257
if (!levelkeys && !this._decryptdata) {

src/loader/m3u8-parser.ts

+10
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ export default class M3U8Parser {
314314
let currentPart = 0;
315315
let totalduration = 0;
316316
let discontinuityCounter = 0;
317+
let currentBitrate = 0;
317318
let prevFrag: Fragment | null = null;
318319
let frag: Fragment = new Fragment(type, base);
319320
let result: RegExpExecArray | RegExpMatchArray | null;
@@ -338,6 +339,9 @@ export default class M3U8Parser {
338339
frag.start = totalduration;
339340
frag.sn = currentSN;
340341
frag.cc = discontinuityCounter;
342+
if (currentBitrate) {
343+
frag.bitrate = currentBitrate;
344+
}
341345
frag.level = id;
342346
if (currentInitSegment) {
343347
frag.initSegment = currentInitSegment;
@@ -481,6 +485,12 @@ export default class M3U8Parser {
481485
break;
482486
case 'BITRATE':
483487
frag.tagList.push([tag, value1]);
488+
currentBitrate = parseInt(value1) * 1000;
489+
if (Number.isFinite(currentBitrate)) {
490+
frag.bitrate = currentBitrate;
491+
} else {
492+
currentBitrate = 0;
493+
}
484494
break;
485495
case 'DATERANGE': {
486496
const dateRangeAttr = new AttrList(value1, level);

tests/unit/loader/playlist-loader.ts

+57
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import chai from 'chai';
22
import sinonChai from 'sinon-chai';
3+
import { LoadStats } from '../../../src/loader/load-stats';
34
import M3U8Parser from '../../../src/loader/m3u8-parser';
45
import { PlaylistLevelType } from '../../../src/types/loader';
56
import { AttrList } from '../../../src/utils/attr-list';
@@ -1688,20 +1689,76 @@ fileSequence2.ts
16881689
null,
16891690
);
16901691
const fragments = details.fragments as Fragment[];
1692+
expect(fragments[0].bitrate).to.equal(5083000);
16911693
expectWithJSONMessage(fragments[0].tagList).to.deep.equal([
16921694
['INF', '5.97263', '\t'],
16931695
['BITRATE', '5083'],
16941696
]);
1697+
1698+
expect(fragments[1].bitrate).to.equal(5453000);
16951699
expectWithJSONMessage(fragments[1].tagList).to.deep.equal([
16961700
['INF', '5.97263', '\t'],
16971701
['BITRATE', '5453'],
16981702
]);
1703+
1704+
expect(fragments[2].bitrate).to.equal(4802000);
16991705
expectWithJSONMessage(fragments[2].tagList).to.deep.equal([
17001706
['INF', '5.97263', '\t'],
17011707
['BITRATE', '4802'],
17021708
]);
17031709
});
17041710

1711+
it('parses segment tags used to get bitrate and byte length from fragments', function () {
1712+
const playlist = `#EXTM3U
1713+
#EXT-X-TARGETDURATION:6
1714+
#EXT-X-VERSION:3
1715+
#EXT-X-MEDIA-SEQUENCE:0
1716+
#EXT-X-PLAYLIST-TYPE:VOD
1717+
#EXTINF:5
1718+
#EXT-X-BITRATE:1000
1719+
fileSequence0.ts
1720+
#EXTINF:5
1721+
#EXT-X-BYTERANGE:600000@123456
1722+
fileSequence1.ts
1723+
#EXTINF: 5
1724+
fileSequence2.ts
1725+
`;
1726+
const details = M3U8Parser.parseLevelPlaylist(
1727+
playlist,
1728+
'http://dummy.url.com/playlist.m3u8',
1729+
0,
1730+
PlaylistLevelType.MAIN,
1731+
0,
1732+
null,
1733+
);
1734+
const fragments = details.fragments as Fragment[];
1735+
expect(fragments[0].byteLength).to.equal(null);
1736+
expect(fragments[0].bitrate).to.equal(1000000);
1737+
expect(fragments[1].byteLength).to.equal(600000);
1738+
expect(fragments[1].bitrate).to.equal(960000);
1739+
expect(fragments[2].byteLength).to.equal(null);
1740+
expect(fragments[2].bitrate).to.equal(
1741+
1000000,
1742+
'#EXT-X-BITRATE applies to every segment between it and the next bitrate tag',
1743+
);
1744+
1745+
// Stat data overrides byteLength and bitrate data
1746+
fragments[2].stats = new LoadStats();
1747+
fragments[2].stats.total = 12000;
1748+
expect(fragments[2].byteLength).to.equal(12000);
1749+
expect(fragments[2].bitrate).to.equal(19200);
1750+
1751+
fragments[1].stats = new LoadStats();
1752+
fragments[1].stats.total = 8000000;
1753+
expect(fragments[1].byteLength).to.equal(8000000);
1754+
expect(fragments[1].bitrate).to.equal(12800000);
1755+
1756+
fragments[0].stats = new LoadStats();
1757+
fragments[0].stats.total = 5000000;
1758+
expect(fragments[0].byteLength).to.equal(5000000);
1759+
expect(fragments[0].bitrate).to.equal(8000000);
1760+
});
1761+
17051762
it('adds GAP to fragment.tagList and sets fragment.gap', function () {
17061763
const playlist = `#EXTM3U
17071764
#EXT-X-TARGETDURATION:5

0 commit comments

Comments
 (0)