Skip to content

Commit

Permalink
Use segment #EXT-X-BITRATE tag value where applicable (#6843)
Browse files Browse the repository at this point in the history
Adds Fragment byteLength getter and bitrate getter/setter
  • Loading branch information
robwalch authored Nov 14, 2024
1 parent 9c5089d commit 4b61208
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 17 deletions.
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,20 +86,18 @@ For details on the HLS format and these tags' meanings, see https://datatracker.
- `#EXT-X-CONTENT-STEERING:<attribute-list>` Content Steering
- `#EXT-X-DEFINE:<attribute-list>` Variable Substitution (`NAME,VALUE,QUERYPARAM` attributes)

The following properties are added to their respective variants' attribute list but are not implemented in their selection and playback.

- `VIDEO-RANGE` (See [#2489](https://github.com/video-dev/hls.js/issues/2489))

#### Media Playlist tags

- `#EXTM3U`
- `#EXT-X-VERSION=<n>`
- `#EXTM3U` (ignored)
- `#EXT-X-INDEPENDENT-SEGMENTS` (ignored)
- `#EXT-X-VERSION=<n>` (value is ignored)
- `#EXTINF:<duration>,[<title>]`
- `#EXT-X-ENDLIST`
- `#EXT-X-MEDIA-SEQUENCE=<n>`
- `#EXT-X-TARGETDURATION=<n>`
- `#EXT-X-DISCONTINUITY`
- `#EXT-X-DISCONTINUITY-SEQUENCE=<n>`
- `#EXT-X-BITRATE`
- `#EXT-X-BYTERANGE=<n>[@<o>]`
- `#EXT-X-MAP:<attribute-list>`
- `#EXT-X-KEY:<attribute-list>` (`KEYFORMAT="identity",METHOD=SAMPLE-AES` is only supports with MPEG-2 TS segments)
Expand All @@ -115,11 +113,7 @@ The following properties are added to their respective variants' attribute list
- `#EXT-X-DEFINE:<attribute-list>` Variable Import and Substitution (`NAME,VALUE,IMPORT,QUERYPARAM` attributes)
- `#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))

The following tags are added to their respective fragment's attribute list but are not implemented in streaming and playback.

- `#EXT-X-BITRATE` (Not used in ABR controller)

Parsed but missing feature support
Parsed but missing feature support:

- `#EXT-X-PRELOAD-HINT:<attribute-list>` (See [#5074](https://github.com/video-dev/hls.js/issues/3988))
- #5074
Expand All @@ -129,6 +123,7 @@ Parsed but missing feature support
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).

- `#EXT-X-I-FRAME-STREAM-INF` I-frame Media Playlist files
- `REQ-VIDEO-LAYOUT` is not used in variant filtering or selection
- "identity" format `SAMPLE-AES` method keys with fmp4, aac, mp3, vtt... segments (MPEG-2 TS only)
- MPEG-2 TS segments with FairPlay Streaming, PlayReady, or Widevine encryption
- FairPlay Streaming legacy keys (For com.apple.fps.1_0 use native Safari playback)
Expand Down
5 changes: 5 additions & 0 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1716,8 +1716,13 @@ export class Fragment extends BaseSegment {
// (undocumented)
addStart(value: number): void;
// (undocumented)
get bitrate(): number | null;
set bitrate(value: number);
// (undocumented)
bitrateTest: boolean;
// (undocumented)
get byteLength(): number | null;
// (undocumented)
cc: number;
// (undocumented)
data?: Uint8Array;
Expand Down
11 changes: 6 additions & 5 deletions src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,10 @@ class AbrController extends Logger implements AbrComponentAPI {
const bwEstimate: number = this.getBwEstimate();
const levels = hls.levels;
const level = levels[frag.level];
const expectedLen =
stats.total ||
Math.max(stats.loaded, Math.round((duration * level.averageBitrate) / 8));
const expectedLen = Math.max(
stats.loaded,
Math.round((duration * (frag.bitrate || level.averageBitrate)) / 8),
);
let timeStreaming = loadedFirstByte ? timeLoading - ttfb : timeLoading;
if (timeStreaming < 1 && loadedFirstByte) {
timeStreaming = Math.min(timeLoading, (stats.loaded * 8) / bwEstimate);
Expand Down Expand Up @@ -880,8 +881,8 @@ class AbrController extends Logger implements AbrComponentAPI {
currentFragDuration &&
bufferStarvationDelay >= currentFragDuration * 2 &&
maxStarvationDelay === 0
? levels[i].averageBitrate
: levels[i].maxBitrate;
? levelInfo.averageBitrate
: levelInfo.maxBitrate;
const fetchDuration: number = this.getTimeToLoadFrag(
ttfbEstimateSec,
adjustedbw,
Expand Down
2 changes: 1 addition & 1 deletion src/loader/fragment-loader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ErrorDetails, ErrorTypes } from '../errors';
import { getLoaderConfigWithoutReties } from '../utils/error-helper';
import type { HlsConfig } from '../config';
import type { BaseSegment, Fragment, Part } from './fragment';
import type { HlsConfig } from '../config';
import type {
ErrorData,
FragLoadedData,
Expand Down
33 changes: 33 additions & 0 deletions src/loader/fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ export class Fragment extends BaseSegment {
private _decryptdata: LevelKey | null = null;
private _programDateTime: number | null = null;
private _ref: MediaFragmentRef | null = null;
// Approximate bit rate of the fragment expressed in bits per second (bps) as indicated by the last EXT-X-BITRATE (kbps) tag
private _bitrate?: number;

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

get byteLength(): number | null {
if (this.hasStats) {
const total = this.stats.total;
if (total) {
return total;
}
}
if (this.byteRange) {
const start = this.byteRange[0];
const end = this.byteRange[1];
if (Number.isFinite(start) && Number.isFinite(end)) {
return (end as number) - (start as number);
}
}
return null;
}

get bitrate(): number | null {
if (this.byteLength) {
return (this.byteLength * 8) / this.duration;
}
if (this._bitrate) {
return this._bitrate;
}
return null;
}

set bitrate(value: number) {
this._bitrate = value;
}

get decryptdata(): LevelKey | null {
const { levelkeys } = this;
if (!levelkeys && !this._decryptdata) {
Expand Down
10 changes: 10 additions & 0 deletions src/loader/m3u8-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,7 @@ export default class M3U8Parser {
let currentPart = 0;
let totalduration = 0;
let discontinuityCounter = 0;
let currentBitrate = 0;
let prevFrag: Fragment | null = null;
let frag: Fragment = new Fragment(type, base);
let result: RegExpExecArray | RegExpMatchArray | null;
Expand All @@ -338,6 +339,9 @@ export default class M3U8Parser {
frag.start = totalduration;
frag.sn = currentSN;
frag.cc = discontinuityCounter;
if (currentBitrate) {
frag.bitrate = currentBitrate;
}
frag.level = id;
if (currentInitSegment) {
frag.initSegment = currentInitSegment;
Expand Down Expand Up @@ -481,6 +485,12 @@ export default class M3U8Parser {
break;
case 'BITRATE':
frag.tagList.push([tag, value1]);
currentBitrate = parseInt(value1) * 1000;
if (Number.isFinite(currentBitrate)) {
frag.bitrate = currentBitrate;
} else {
currentBitrate = 0;
}
break;
case 'DATERANGE': {
const dateRangeAttr = new AttrList(value1, level);
Expand Down
57 changes: 57 additions & 0 deletions tests/unit/loader/playlist-loader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import chai from 'chai';
import sinonChai from 'sinon-chai';
import { LoadStats } from '../../../src/loader/load-stats';
import M3U8Parser from '../../../src/loader/m3u8-parser';
import { PlaylistLevelType } from '../../../src/types/loader';
import { AttrList } from '../../../src/utils/attr-list';
Expand Down Expand Up @@ -1688,20 +1689,76 @@ fileSequence2.ts
null,
);
const fragments = details.fragments as Fragment[];
expect(fragments[0].bitrate).to.equal(5083000);
expectWithJSONMessage(fragments[0].tagList).to.deep.equal([
['INF', '5.97263', '\t'],
['BITRATE', '5083'],
]);

expect(fragments[1].bitrate).to.equal(5453000);
expectWithJSONMessage(fragments[1].tagList).to.deep.equal([
['INF', '5.97263', '\t'],
['BITRATE', '5453'],
]);

expect(fragments[2].bitrate).to.equal(4802000);
expectWithJSONMessage(fragments[2].tagList).to.deep.equal([
['INF', '5.97263', '\t'],
['BITRATE', '4802'],
]);
});

it('parses segment tags used to get bitrate and byte length from fragments', function () {
const playlist = `#EXTM3U
#EXT-X-TARGETDURATION:6
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:5
#EXT-X-BITRATE:1000
fileSequence0.ts
#EXTINF:5
#EXT-X-BYTERANGE:600000@123456
fileSequence1.ts
#EXTINF: 5
fileSequence2.ts
`;
const details = M3U8Parser.parseLevelPlaylist(
playlist,
'http://dummy.url.com/playlist.m3u8',
0,
PlaylistLevelType.MAIN,
0,
null,
);
const fragments = details.fragments as Fragment[];
expect(fragments[0].byteLength).to.equal(null);
expect(fragments[0].bitrate).to.equal(1000000);
expect(fragments[1].byteLength).to.equal(600000);
expect(fragments[1].bitrate).to.equal(960000);
expect(fragments[2].byteLength).to.equal(null);
expect(fragments[2].bitrate).to.equal(
1000000,
'#EXT-X-BITRATE applies to every segment between it and the next bitrate tag',
);

// Stat data overrides byteLength and bitrate data
fragments[2].stats = new LoadStats();
fragments[2].stats.total = 12000;
expect(fragments[2].byteLength).to.equal(12000);
expect(fragments[2].bitrate).to.equal(19200);

fragments[1].stats = new LoadStats();
fragments[1].stats.total = 8000000;
expect(fragments[1].byteLength).to.equal(8000000);
expect(fragments[1].bitrate).to.equal(12800000);

fragments[0].stats = new LoadStats();
fragments[0].stats.total = 5000000;
expect(fragments[0].byteLength).to.equal(5000000);
expect(fragments[0].bitrate).to.equal(8000000);
});

it('adds GAP to fragment.tagList and sets fragment.gap', function () {
const playlist = `#EXTM3U
#EXT-X-TARGETDURATION:5
Expand Down

0 comments on commit 4b61208

Please sign in to comment.