Skip to content

Commit

Permalink
Handle option grouping for EXT-X-STREAM-INF with same URL and attributes
Browse files Browse the repository at this point in the history
Add VIDEO_RANGE and HDCP attriubute values to Redundant Stream grouping
Add channels and characteristics to MediaPlaylist
Resolves #5302
  • Loading branch information
robwalch committed Oct 3, 2023
1 parent 53320e0 commit f37400b
Show file tree
Hide file tree
Showing 20 changed files with 527 additions and 201 deletions.
12 changes: 10 additions & 2 deletions api-extractor/report/hls.js.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2005,6 +2005,8 @@ export class Level {
// (undocumented)
addFallback(data: LevelParsed): void;
// (undocumented)
addGroupId(type: string, groupId: string | undefined, fallbackIndex: number): void;
// (undocumented)
get attrs(): LevelAttributes;
// (undocumented)
readonly _attrs: LevelAttributes[];
Expand All @@ -2015,6 +2017,8 @@ export class Level {
// (undocumented)
audioGroupIds?: (string | undefined)[];
// (undocumented)
get audioGroups(): (string | undefined)[] | undefined;
// (undocumented)
get averageBitrate(): number;
// (undocumented)
readonly bitrate: number;
Expand Down Expand Up @@ -2050,6 +2054,8 @@ export class Level {
// (undocumented)
get score(): number;
// (undocumented)
get subtitleGroups(): (string | undefined)[] | undefined;
// (undocumented)
supportedPromise?: Promise<MediaDecodingInfo>;
// (undocumented)
supportedResult?: MediaDecodingInfo;
Expand Down Expand Up @@ -2737,6 +2743,10 @@ export interface MediaPlaylist extends Omit<LevelParsed, 'attrs'> {
// (undocumented)
autoselect: boolean;
// (undocumented)
channels?: string;
// (undocumented)
characteristics?: string;
// (undocumented)
default: boolean;
// (undocumented)
forced: boolean;
Expand Down Expand Up @@ -3050,8 +3060,6 @@ export class SubtitleStreamController extends BaseStreamController implements Ne
// (undocumented)
_handleFragmentLoadComplete(fragLoadedData: FragLoadedData): void;
// (undocumented)
protected levels: Array<Level>;
// (undocumented)
protected loadFragment(frag: Fragment, level: Level, targetBufferTime: number): void;
// (undocumented)
get mediaBufferTimeRanges(): Bufferable;
Expand Down
2 changes: 1 addition & 1 deletion src/controller/abr-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ class AbrController implements AbrComponentAPI {
this.lastLoadedFragLevel = -1;
this.lastLevelLoadSec = 0;
this.fragCurrent = this.partCurrent = null;
this.audioTracksByGroup = null;
this.onLevelsUpdated();
this.clearTimer();
}
Expand All @@ -123,6 +122,7 @@ class AbrController implements AbrComponentAPI {
this._nextAutoLevel = -1;
this.nextAutoLevelKey = '';
this.codecTiers = null;
this.audioTracksByGroup = null;
}

protected onFragLoading(event: Events.FRAG_LOADING, data: FragLoadingData) {
Expand Down
51 changes: 38 additions & 13 deletions src/controller/audio-stream-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import TransmuxerInterface from '../demux/transmuxer-interface';
import { ChunkMetadata } from '../types/transmuxer';
import { fragmentWithinToleranceTest } from './fragment-finders';
import { alignMediaPlaylistByPDT } from '../utils/discontinuities';
import { mediaAttributesIdentical } from '../utils/media-option-attributes';
import { ErrorDetails } from '../errors';
import type { NetworkComponentAPI } from '../types/component-api';
import type Hls from '../hls';
Expand All @@ -32,6 +33,7 @@ import type {
FragParsingUserdataData,
FragBufferedData,
ErrorData,
BufferFlushingData,
} from '../types/events';
import type { MediaPlaylist } from '../types/media-playlist';

Expand All @@ -56,6 +58,7 @@ class AudioStreamController
private trackId: number = -1;
private waitingData: WaitingForPTSData | null = null;
private mainDetails: LevelDetails | null = null;
private flushing: boolean = false;
private bufferFlushed: boolean = false;
private cachedTrackLoadedData: TrackLoadedData | null = null;

Expand Down Expand Up @@ -93,6 +96,7 @@ class AudioStreamController
hls.on(Events.ERROR, this.onError, this);
hls.on(Events.BUFFER_RESET, this.onBufferReset, this);
hls.on(Events.BUFFER_CREATED, this.onBufferCreated, this);
hls.on(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
hls.on(Events.BUFFER_FLUSHED, this.onBufferFlushed, this);
hls.on(Events.INIT_PTS_FOUND, this.onInitPtsFound, this);
hls.on(Events.FRAG_BUFFERED, this.onFragBuffered, this);
Expand All @@ -110,6 +114,7 @@ class AudioStreamController
hls.off(Events.ERROR, this.onError, this);
hls.off(Events.BUFFER_RESET, this.onBufferReset, this);
hls.off(Events.BUFFER_CREATED, this.onBufferCreated, this);
hls.off(Events.BUFFER_FLUSHING, this.onBufferFlushing, this);
hls.off(Events.BUFFER_FLUSHED, this.onBufferFlushed, this);
hls.off(Events.INIT_PTS_FOUND, this.onInitPtsFound, this);
hls.off(Events.FRAG_BUFFERED, this.onFragBuffered, this);
Expand Down Expand Up @@ -275,15 +280,15 @@ class AudioStreamController
const { hls, levels, media, trackId } = this;
const config = hls.config;

if (!levels?.[trackId]) {
return;
}

// if video not attached AND
// start fragment already requested OR start frag prefetch not enabled
// exit loop
// 1. if video not attached AND
// start fragment already requested OR start frag prefetch not enabled
// 2. if tracks or track not loaded and selected
// then exit loop
// => if media not attached but start frag prefetch is enabled and start frag not requested yet, we will not exit loop
if (!media && (this.startFragRequested || !config.startFragPrefetch)) {
if (
(!media && (this.startFragRequested || !config.startFragPrefetch)) ||
!levels?.[trackId]
) {
return;
}

Expand Down Expand Up @@ -333,11 +338,17 @@ class AudioStreamController

const fragments = trackDetails.fragments;
const start = fragments[0].start;
let targetBufferTime = bufferInfo.end;
let targetBufferTime = this.flushing
? this.getLoadPosition()
: bufferInfo.end;

if (switchingTrack && media) {
const pos = this.getLoadPosition();
if (bufferedTrack && switchingTrack.attrs !== bufferedTrack.attrs) {
// STABLE
if (
bufferedTrack &&
!mediaAttributesIdentical(switchingTrack.attrs, bufferedTrack.attrs)
) {
targetBufferTime = pos;
}
// if currentTime (pos) is less than alt audio playlist start time, it means that alt audio is ahead of currentTime
Expand Down Expand Up @@ -420,13 +431,15 @@ class AudioStreamController

onMediaDetaching() {
this.videoBuffer = null;
this.bufferFlushed = this.flushing = false;
super.onMediaDetaching();
}

onAudioTracksUpdated(
event: Events.AUDIO_TRACKS_UPDATED,
{ audioTracks }: AudioTracksUpdatedData,
) {
// Reset tranxmuxer is essential for large context switches (Content Steering)
this.resetTransmuxer();
this.levels = audioTracks.map((mediaPlaylist) => new Level(mediaPlaylist));
}
Expand Down Expand Up @@ -470,7 +483,7 @@ class AudioStreamController
onManifestLoading() {
this.fragmentTracker.removeAllFragments();
this.startPosition = this.lastCurrentTime = 0;
this.bufferFlushed = false;
this.bufferFlushed = this.flushing = false;
this.levels =
this.mainDetails =
this.waitingData =
Expand Down Expand Up @@ -502,7 +515,9 @@ class AudioStreamController
return;
}
this.log(
`Track ${trackId} loaded [${newDetails.startSN},${newDetails.endSN}]${
`Audio track ${trackId} loaded [${newDetails.startSN},${
newDetails.endSN
}]${
newDetails.lastPartSn
? `[part-${newDetails.lastPartSn}-${newDetails.lastPartIndex}]`
: ''
Expand Down Expand Up @@ -746,11 +761,21 @@ class AudioStreamController
}
}

private onBufferFlushing(
event: Events.BUFFER_FLUSHING,
{ type }: BufferFlushingData,
) {
if (type !== ElementaryStreamTypes.VIDEO) {
this.flushing = true;
}
}

private onBufferFlushed(
event: Events.BUFFER_FLUSHED,
{ type }: BufferFlushedData,
) {
if (type === ElementaryStreamTypes.AUDIO) {
if (type !== ElementaryStreamTypes.VIDEO) {
this.flushing = false;
this.bufferFlushed = true;
if (this.state === State.ENDED) {
this.state = State.IDLE;
Expand Down
81 changes: 49 additions & 32 deletions src/controller/audio-track-controller.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import BasePlaylistController from './base-playlist-controller';
import { Events } from '../events';
import { ErrorTypes, ErrorDetails } from '../errors';
import {
import { PlaylistContextType } from '../types/loader';
import { mediaAttributesIdentical } from '../utils/media-option-attributes';
import type Hls from '../hls';
import type { MediaPlaylist } from '../types/media-playlist';
import type { HlsUrlParameters } from '../types/level';
import type {
ManifestParsedData,
AudioTracksUpdatedData,
ErrorData,
LevelLoadingData,
AudioTrackLoadedData,
LevelSwitchingData,
} from '../types/events';
import BasePlaylistController from './base-playlist-controller';
import { PlaylistContextType } from '../types/loader';
import type Hls from '../hls';
import type { HlsUrlParameters } from '../types/level';
import type { MediaPlaylist } from '../types/media-playlist';

class AudioTrackController extends BasePlaylistController {
private tracks: MediaPlaylist[] = [];
private groupId: string | null = null;
private groupIds: (string | undefined)[] | null = null;
private tracksInGroup: MediaPlaylist[] = [];
private trackId: number = -1;
private currentTrack: MediaPlaylist | null = null;
Expand Down Expand Up @@ -57,7 +58,7 @@ class AudioTrackController extends BasePlaylistController {

protected onManifestLoading(): void {
this.tracks = [];
this.groupId = null;
this.groupIds = null;
this.tracksInGroup = [];
this.trackId = -1;
this.currentTrack = null;
Expand All @@ -80,15 +81,15 @@ class AudioTrackController extends BasePlaylistController {

if (!trackInActiveGroup || trackInActiveGroup.groupId !== groupId) {
this.warn(
`Track with id:${id} and group:${groupId} not found in active group ${trackInActiveGroup.groupId}`,
`Audio track with id:${id} and group:${groupId} not found in active group ${trackInActiveGroup?.groupId}`,
);
return;
}

const curDetails = trackInActiveGroup.details;
trackInActiveGroup.details = data.details;
this.log(
`audio-track ${id} "${trackInActiveGroup.name}" lang:${trackInActiveGroup.lang} group:${groupId} loaded [${details.startSN}-${details.endSN}]`,
`Audio track ${id} "${trackInActiveGroup.name}" lang:${trackInActiveGroup.lang} group:${groupId} loaded [${details.startSN}-${details.endSN}]`,
);

if (id === this.trackId) {
Expand All @@ -113,16 +114,22 @@ class AudioTrackController extends BasePlaylistController {
private switchLevel(levelIndex: number) {
const levelInfo = this.hls.levels[levelIndex];

if (!levelInfo?.audioGroupIds) {
if (!levelInfo) {
return;
}

const audioGroupId = levelInfo.audioGroupIds[levelInfo.urlId];
if (this.groupId !== audioGroupId) {
this.groupId = audioGroupId || null;
const audioGroups = levelInfo.audioGroups || null;
const currentGroups = this.groupIds;
if (
!audioGroups ||
currentGroups?.length !== audioGroups?.length ||
audioGroups?.some((groupId) => currentGroups?.indexOf(groupId) === -1)
) {
this.groupIds = audioGroups;

const audioTracks = this.tracks.filter(
(track): boolean => !audioGroupId || track.groupId === audioGroupId,
(track): boolean =>
!audioGroups || audioGroups.indexOf(track.groupId) !== -1,
);

// Disable selectDefaultTrack if there are no default tracks
Expand All @@ -136,10 +143,15 @@ class AudioTrackController extends BasePlaylistController {
this.trackId = -1;
this.currentTrack = null;
}
audioTracks.forEach((track, i) => {
track.id = i;
});
this.tracksInGroup = audioTracks;
const audioTracksUpdated: AudioTracksUpdatedData = { audioTracks };
this.log(
`Updating audio tracks, ${audioTracks.length} track(s) found in group:${audioGroupId}`,
`Updating audio tracks, ${
audioTracks.length
} track(s) found in group(s): ${audioGroups?.join(',')}`,
);
this.hls.trigger(Events.AUDIO_TRACKS_UPDATED, audioTracksUpdated);

Expand All @@ -158,7 +170,7 @@ class AudioTrackController extends BasePlaylistController {
if (
data.context.type === PlaylistContextType.AUDIO_TRACK &&
data.context.id === this.trackId &&
data.context.groupId === this.groupId
(!this.groupIds || this.groupIds.indexOf(data.context.groupId) !== -1)
) {
this.requestScheduled = -1;
this.checkRetry(data);
Expand Down Expand Up @@ -188,26 +200,28 @@ class AudioTrackController extends BasePlaylistController {

// check if level idx is valid
if (newId < 0 || newId >= tracks.length) {
this.warn('Invalid id passed to audio-track controller');
this.warn(`Invalid audio track id: ${newId}`);
return;
}

// stopping live reloading timer if any
this.clearTimer();

this.selectDefaultTrack = false;
const lastTrack = this.currentTrack;
tracks[this.trackId];
const track = tracks[newId];
const { groupId, name } = track;
const trackLoaded = track.details && !track.details.live;
if (newId === this.trackId && track === lastTrack && trackLoaded) {
return;
}
this.log(
`Switching to audio-track ${newId} "${name}" lang:${track.lang} group:${groupId}`,
`Switching to audio-track ${newId} "${track.name}" lang:${track.lang} group:${track.groupId}`,
);
this.trackId = newId;
this.currentTrack = track;
this.selectDefaultTrack = false;
this.hls.trigger(Events.AUDIO_TRACK_SWITCHING, { ...track });
// Do not reload track unless live
if (track.details && !track.details.live) {
if (trackLoaded) {
return;
}
const hlsUrlParameters = this.switchParams(track.url, lastTrack?.details);
Expand All @@ -228,7 +242,9 @@ class AudioTrackController extends BasePlaylistController {
this.setAudioTrack(trackId);
} else {
const error = new Error(
`No track found for running audio group-ID: ${this.groupId} track count: ${audioTracks.length}`,
`No track found for running audio group-ID(s): ${this.groupIds?.join(
',',
)} track count: ${audioTracks.length}`,
);
this.warn(error.message);

Expand All @@ -248,15 +264,16 @@ class AudioTrackController extends BasePlaylistController {
if (!this.selectDefaultTrack || track.default) {
if (
!currentTrack ||
(currentTrack.attrs['STABLE-RENDITION-ID'] !== undefined &&
currentTrack.attrs['STABLE-RENDITION-ID'] ===
track.attrs['STABLE-RENDITION-ID'])
mediaAttributesIdentical(currentTrack.attrs, track.attrs)
) {
return track.id;
}
if (
currentTrack.name === track.name &&
currentTrack.lang === track.lang
mediaAttributesIdentical(currentTrack.attrs, track.attrs, [
'LANGUAGE',
'ASSOC-LANGUAGE',
'CHARACTERISTICS',
])
) {
return track.id;
}
Expand All @@ -266,9 +283,9 @@ class AudioTrackController extends BasePlaylistController {
}

protected loadPlaylist(hlsUrlParameters?: HlsUrlParameters): void {
super.loadPlaylist();
const audioTrack = this.tracksInGroup[this.trackId];
if (this.shouldLoadPlaylist(audioTrack)) {
const audioTrack = this.currentTrack;
if (this.shouldLoadPlaylist(audioTrack) && audioTrack) {
super.loadPlaylist();
const id = audioTrack.id;
const groupId = audioTrack.groupId as string;
let url = audioTrack.url;
Expand Down
Loading

0 comments on commit f37400b

Please sign in to comment.