Skip to content

Commit 7ce13b6

Browse files
committed
Create media-fragment-controller for temporal support
1 parent 81bca3e commit 7ce13b6

12 files changed

Lines changed: 345 additions & 337 deletions

File tree

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2159,8 +2159,6 @@ class Hls implements HlsEventEmitter {
21592159
// (undocumented)
21602160
get media(): HTMLMediaElement | null;
21612161
// (undocumented)
2162-
get mediaFragment(): TemporalFragment | undefined;
2163-
// (undocumented)
21642162
static get MetadataSchema(): typeof MetadataSchema;
21652163
get minAutoLevel(): number;
21662164
get nextAudioTrack(): number;
@@ -2343,6 +2341,7 @@ export type HlsConfig = {
23432341
capLevelController: typeof CapLevelController;
23442342
errorController: typeof ErrorController;
23452343
fpsController: typeof FPSController;
2344+
mediaFragmentController: typeof MediaFragmentController;
23462345
progressive: boolean;
23472346
lowLatencyMode: boolean;
23482347
primarySessionId?: string;
@@ -4048,6 +4047,15 @@ export interface MediaFragment extends Fragment {
40484047
sn: number;
40494048
}
40504049

4050+
// Warning: (ae-missing-release-tag) "MediaFragmentController" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
4051+
//
4052+
// @public
4053+
export class MediaFragmentController extends Logger implements ComponentAPI {
4054+
constructor(hls: Hls);
4055+
// (undocumented)
4056+
destroy(): void;
4057+
}
4058+
40514059
// Warning: (ae-missing-release-tag) "MediaKeyFunc" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal)
40524060
//
40534061
// @public (undocumented)

src/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import EMEController from './controller/eme-controller';
99
import ErrorController from './controller/error-controller';
1010
import FPSController from './controller/fps-controller';
1111
import InterstitialsController from './controller/interstitials-controller';
12+
import MediaFragmentController from './controller/media-fragment-controller';
1213
import { SubtitleStreamController } from './controller/subtitle-stream-controller';
1314
import SubtitleTrackController from './controller/subtitle-track-controller';
1415
import { TimelineController } from './controller/timeline-controller';
@@ -338,6 +339,7 @@ export type HlsConfig = {
338339
capLevelController: typeof CapLevelController;
339340
errorController: typeof ErrorController;
340341
fpsController: typeof FPSController;
342+
mediaFragmentController: typeof MediaFragmentController;
341343
progressive: boolean;
342344
lowLatencyMode: boolean;
343345
primarySessionId?: string;
@@ -434,6 +436,7 @@ export const hlsDefaultConfig: HlsConfig = {
434436
capLevelController: CapLevelController,
435437
errorController: ErrorController,
436438
fpsController: FPSController,
439+
mediaFragmentController: MediaFragmentController,
437440
stretchShortVideoTrack: false, // used by mp4-remuxer
438441
maxAudioFramesDrift: 1, // used by mp4-remuxer
439442
forceKeyFrameOnDiscontinuity: true, // used by ts-demuxer
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import { Events } from '../events';
2+
import { Logger } from '../utils/logger';
3+
import { parseMediaFragment } from '../utils/media-fragment-parser';
4+
import type Hls from '../hls';
5+
import type { ComponentAPI } from '../types/component-api';
6+
import type {
7+
ManifestLoadingData,
8+
MediaAttachingData,
9+
MediaDetachingData,
10+
} from '../types/events';
11+
12+
/**
13+
* MediaFragmentController
14+
*
15+
* Handles W3C Media Fragments URI temporal dimension (#t=start,end).
16+
* - Parses fragment from URL
17+
* - Sets start position
18+
* - Pauses at end time (one-time)
19+
* - Removes listeners after pause
20+
*/
21+
export default class MediaFragmentController
22+
extends Logger
23+
implements ComponentAPI
24+
{
25+
private hls: Hls;
26+
private media: HTMLMediaElement | null = null;
27+
private fragmentEnd: number | null = null;
28+
private endReached: boolean = false;
29+
private _boundOnTimeUpdate: () => void;
30+
private _boundOnSeeked: () => void;
31+
32+
constructor(hls: Hls) {
33+
super('media-fragment', hls.logger);
34+
this.hls = hls;
35+
this._boundOnTimeUpdate = this.onTimeUpdate.bind(this);
36+
this._boundOnSeeked = this.onSeeked.bind(this);
37+
this.registerListeners();
38+
}
39+
40+
private registerListeners() {
41+
const { hls } = this;
42+
hls.on(Events.MANIFEST_LOADING, this.onManifestLoading, this);
43+
hls.on(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
44+
hls.on(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
45+
}
46+
47+
private unregisterListeners() {
48+
const { hls } = this;
49+
hls.off(Events.MANIFEST_LOADING, this.onManifestLoading, this);
50+
hls.off(Events.MEDIA_ATTACHING, this.onMediaAttaching, this);
51+
hls.off(Events.MEDIA_DETACHING, this.onMediaDetaching, this);
52+
}
53+
54+
private onManifestLoading(
55+
event: Events.MANIFEST_LOADING,
56+
data: ManifestLoadingData,
57+
) {
58+
if (!data.url.includes('#')) {
59+
return;
60+
}
61+
const { temporalFragment } = parseMediaFragment(data.url);
62+
this.fragmentEnd = null;
63+
this.endReached = false;
64+
this.detachMediaListeners();
65+
if (temporalFragment) {
66+
if (temporalFragment.start !== undefined) {
67+
this.hls.config.startPosition = temporalFragment.start;
68+
}
69+
this.fragmentEnd = temporalFragment.end ?? null;
70+
this.hls.trigger(Events.MEDIA_FRAGMENT_PARSED, {
71+
start: temporalFragment.start,
72+
end: temporalFragment.end,
73+
});
74+
if (this.media && this.fragmentEnd !== null) {
75+
this.attachMediaListeners();
76+
}
77+
}
78+
}
79+
80+
private onMediaAttaching(
81+
event: Events.MEDIA_ATTACHING,
82+
data: MediaAttachingData,
83+
) {
84+
this.media = data.media;
85+
if (this.fragmentEnd !== null && !this.endReached) {
86+
this.attachMediaListeners();
87+
}
88+
}
89+
90+
private onMediaDetaching(
91+
event: Events.MEDIA_DETACHING,
92+
data: MediaDetachingData,
93+
) {
94+
this.detachMediaListeners();
95+
this.media = null;
96+
}
97+
98+
private attachMediaListeners() {
99+
if (!this.media) {
100+
return;
101+
}
102+
this.media.addEventListener('timeupdate', this._boundOnTimeUpdate);
103+
this.media.addEventListener('seeked', this._boundOnSeeked);
104+
}
105+
106+
private detachMediaListeners() {
107+
if (this.media) {
108+
this.media.removeEventListener('timeupdate', this._boundOnTimeUpdate);
109+
this.media.removeEventListener('seeked', this._boundOnSeeked);
110+
}
111+
}
112+
113+
private onTimeUpdate() {
114+
this.checkFragmentEnd();
115+
}
116+
117+
private onSeeked() {
118+
const { media } = this;
119+
if (media) {
120+
this.checkFragmentEnd(media.currentTime);
121+
}
122+
}
123+
124+
private checkFragmentEnd(seekTime?: number) {
125+
const { media, fragmentEnd, endReached } = this;
126+
if (!media || fragmentEnd === null || endReached) {
127+
return;
128+
}
129+
const time = seekTime ?? media.currentTime;
130+
if (time >= fragmentEnd && (!media.paused || seekTime !== undefined)) {
131+
this.log(
132+
`Reached media fragment end at ${time.toFixed(3)} (end: ${fragmentEnd.toFixed(3)})`,
133+
);
134+
this.endReached = true;
135+
media.pause();
136+
this.detachMediaListeners();
137+
this.hls.trigger(Events.MEDIA_FRAGMENT_END, {});
138+
}
139+
}
140+
141+
destroy() {
142+
this.unregisterListeners();
143+
this.detachMediaListeners();
144+
this.media = null;
145+
// @ts-ignore
146+
this.hls = null;
147+
}
148+
}

src/controller/stream-controller.ts

Lines changed: 0 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ export default class StreamController
7373
private _backtrackFragment: Fragment | undefined = undefined;
7474
private audioCodecSwitch: boolean = false;
7575
private videoBuffer: ExtendedSourceBuffer | null = null;
76-
private _mediaFragmentEndReached: boolean = false;
7776

7877
constructor(
7978
hls: Hls,
@@ -231,7 +230,6 @@ export default class StreamController
231230
this.lastCurrentTime = this.media.currentTime;
232231
}
233232
this.checkFragmentChanged();
234-
this._checkMediaFragmentEnd();
235233
}
236234

237235
private doTickIdle() {
@@ -445,28 +443,6 @@ export default class StreamController
445443
return true;
446444
}
447445

448-
private _checkMediaFragmentEnd(currentTime?: number): void {
449-
const { media, hls } = this;
450-
const fragment = hls.mediaFragment;
451-
if (!media || !fragment?.end) {
452-
return;
453-
}
454-
const time = currentTime ?? media.currentTime;
455-
// Only pause if:
456-
// 1. Playing (or seeking past end)
457-
// 2. Haven't reached end yet
458-
// 3. Currently at or past the end time
459-
if (
460-
!this._mediaFragmentEndReached &&
461-
time >= fragment.end &&
462-
(!media.paused || currentTime !== undefined)
463-
) {
464-
this._mediaFragmentEndReached = true;
465-
media.pause();
466-
hls.trigger(Events.MEDIA_FRAGMENT_END, {});
467-
}
468-
}
469-
470446
/**
471447
* Get backtrack fragment. Override to return actual backtrack fragment.
472448
*/
@@ -550,8 +526,6 @@ export default class StreamController
550526

551527
this.log(`Media seeked to ${currentTime.toFixed(3)}`);
552528

553-
this._checkMediaFragmentEnd(currentTime);
554-
555529
// If seeked was issued before buffer was appended do not tick immediately
556530
if (!this.getBufferedFrag(currentTime)) {
557531
return;
@@ -584,7 +558,6 @@ export default class StreamController
584558
this.backtrackFragment = undefined;
585559
this.altAudio = AlternateAudio.DISABLED;
586560
this.audioOnly = false;
587-
this._mediaFragmentEndReached = false;
588561
}
589562

590563
private onManifestParsed(

src/exports-named.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import ContentSteeringController from './controller/content-steering-controller'
1010
import EMEController from './controller/eme-controller';
1111
import ErrorController from './controller/error-controller';
1212
import FPSController from './controller/fps-controller';
13+
import MediaFragmentController from './controller/media-fragment-controller';
1314
import SubtitleTrackController from './controller/subtitle-track-controller';
1415
import Hls from './hls';
1516
import M3U8Parser from './loader/m3u8-parser';
@@ -33,6 +34,7 @@ export {
3334
EMEController,
3435
ErrorController,
3536
FPSController,
37+
MediaFragmentController,
3638
SubtitleTrackController,
3739
XhrLoader,
3840
FetchLoader,

src/hls.ts

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ import { MetadataSchema } from './types/demuxer';
1717
import { type HdcpLevel, isHdcpLevel, type Level } from './types/level';
1818
import { PlaylistLevelType } from './types/loader';
1919
import { enableLogs, type ILogger } from './utils/logger';
20-
import {
21-
parseMediaFragment,
22-
type TemporalFragment,
23-
} from './utils/media-fragment-parser';
2420
import { getMediaDecodingInfoPromise } from './utils/mediacapabilities-helper';
2521
import { getMediaSource } from './utils/mediasource-helper';
2622
import { getAudioTracksByGroup } from './utils/rendition-helper';
@@ -41,6 +37,7 @@ import type ErrorController from './controller/error-controller';
4137
import type FPSController from './controller/fps-controller';
4238
import type InterstitialsController from './controller/interstitials-controller';
4339
import type { InterstitialsManager } from './controller/interstitials-controller';
40+
import type MediaFragmentController from './controller/media-fragment-controller';
4441
import type { SubtitleStreamController } from './controller/subtitle-stream-controller';
4542
import type SubtitleTrackController from './controller/subtitle-track-controller';
4643
import type Decrypter from './crypt/decrypter';
@@ -108,6 +105,7 @@ export default class Hls implements HlsEventEmitter {
108105
private audioTrackController?: AudioTrackController;
109106
private subtitleTrackController?: SubtitleTrackController;
110107
private interstitialsController?: InterstitialsController;
108+
private mediaFragmentController?: MediaFragmentController;
111109
private gapController: GapController;
112110
private emeController?: EMEController;
113111
private cmcdController?: CMCDController;
@@ -116,7 +114,6 @@ export default class Hls implements HlsEventEmitter {
116114
private _sessionId?: string;
117115
private triggeringException?: boolean;
118116
private started: boolean = false;
119-
private _mediaFragment?: TemporalFragment;
120117

121118
/**
122119
* Get the video-dev/hls.js package version.
@@ -321,6 +318,11 @@ export default class Hls implements HlsEventEmitter {
321318
coreComponents,
322319
);
323320

321+
this.mediaFragmentController = this.createController(
322+
config.mediaFragmentController,
323+
coreComponents,
324+
);
325+
324326
this.coreComponents = coreComponents;
325327

326328
// Error controller handles errors before and after all other controllers
@@ -510,35 +512,16 @@ export default class Hls implements HlsEventEmitter {
510512
this.stopLoad();
511513
const media = this.media;
512514
const loadedSource = this._url;
513-
514-
const { url: cleanUrl, temporal } = parseMediaFragment(url);
515-
516-
if (temporal) {
517-
this._mediaFragment = temporal;
518-
// Media fragment start time takes precedence over config
519-
if (temporal.start !== undefined) {
520-
this.config.startPosition = temporal.start;
521-
}
522-
this.trigger(Events.MEDIA_FRAGMENT_PARSED, {
523-
start: temporal.start,
524-
end: temporal.end,
525-
});
526-
} else {
527-
this._mediaFragment = undefined;
528-
}
529-
530515
const loadingSource = (this._url = buildAbsoluteURL(
531516
self.location.href,
532-
cleanUrl,
517+
url,
533518
{
534519
alwaysNormalize: true,
535520
},
536521
));
537-
538522
this._autoLevelCapping = -1;
539523
this._maxHdcpLevel = null;
540524
this.logger.log(`loadSource:${loadingSource}`);
541-
542525
if (
543526
media &&
544527
loadedSource &&
@@ -549,7 +532,7 @@ export default class Hls implements HlsEventEmitter {
549532
this.attachMedia(media);
550533
}
551534
// when attaching to a source URL, trigger a playlist load
552-
this.trigger(Events.MANIFEST_LOADING, { url: cleanUrl });
535+
this.trigger(Events.MANIFEST_LOADING, { url });
553536
}
554537

555538
/**
@@ -1042,10 +1025,6 @@ export default class Hls implements HlsEventEmitter {
10421025
return this.streamController.maxBufferLength;
10431026
}
10441027

1045-
public get mediaFragment(): TemporalFragment | undefined {
1046-
return this._mediaFragment;
1047-
}
1048-
10491028
/**
10501029
* Find and select the best matching audio track, making a level switch when a Group change is necessary.
10511030
* Updates `hls.config.audioPreference`. Returns the selected track, or null when no matching track is found.
@@ -1331,6 +1310,7 @@ export type {
13311310
ErrorController,
13321311
FPSController,
13331312
InterstitialsController,
1313+
MediaFragmentController,
13341314
StreamController,
13351315
SubtitleStreamController,
13361316
SubtitleTrackController,

0 commit comments

Comments
 (0)