Skip to content

Commit d6d9282

Browse files
committed
FLAC and Opus support
This adds the MP4-registered codec entry "fLaC", as well as the non-registered codec entries "flac" and "FLAC". Browsers added FLAC support before the MP4 Registration Authority listed the codec entry "fLaC", and went with "flac". Bugs have been opened with browsers to address this, but in order to deal with legacy playlist manifests (they may list "flac" or even "FLAC"), this adds a new interface and utility method to generate fallback names, and replace them as they're encountered.
1 parent 9d5ee3e commit d6d9282

8 files changed

+131
-3
lines changed

src/controller/level-controller.ts

+19
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import { isCodecSupportedInMp4 } from '../utils/codecs';
1818
import { addGroupId, assignTrackIdsByGroup } from './level-helper';
1919
import BasePlaylistController from './base-playlist-controller';
2020
import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
21+
import { getCodecCompatNames } from '../utils/codecs';
2122
import type Hls from '../hls';
2223
import type { HlsUrlParameters, LevelParsed } from '../types/level';
2324
import type { MediaPlaylist } from '../types/media-playlist';
25+
import type { CodecCompatNames } from '../utils/codecs';
2426

2527
const chromeOrFirefox: boolean = /chrome|firefox/.test(
2628
navigator.userAgent.toLowerCase()
@@ -89,6 +91,7 @@ export default class LevelController extends BasePlaylistController {
8991
let resolutionFound = false;
9092
let videoCodecFound = false;
9193
let audioCodecFound = false;
94+
const codecCompatNames: CodecCompatNames = getCodecCompatNames();
9295

9396
// regroup redundant levels together
9497
data.levels.forEach((levelParsed: LevelParsed) => {
@@ -109,6 +112,22 @@ export default class LevelController extends BasePlaylistController {
109112
levelParsed.audioCodec = undefined;
110113
}
111114

115+
// replace the video codec name if required
116+
if (
117+
levelParsed.videoCodec &&
118+
levelParsed.videoCodec in codecCompatNames.video
119+
) {
120+
levelParsed.videoCodec = codecCompatNames.video[levelParsed.videoCodec];
121+
}
122+
123+
// replace the audio codec name if required
124+
if (
125+
levelParsed.audioCodec &&
126+
levelParsed.audioCodec in codecCompatNames.audio
127+
) {
128+
levelParsed.audioCodec = codecCompatNames.audio[levelParsed.audioCodec];
129+
}
130+
112131
const levelKey = `${levelParsed.bitrate}-${levelParsed.attrs.RESOLUTION}-${levelParsed.attrs.CODECS}`;
113132
levelFromSet = levelSet[levelKey];
114133

src/controller/stream-controller.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -1210,7 +1210,12 @@ export default class StreamController
12101210
}
12111211
}
12121212
// HE-AAC is broken on Android, always signal audio codec as AAC even if variant manifest states otherwise
1213-
if (ua.indexOf('android') !== -1 && audio.container !== 'audio/mpeg') {
1213+
if (
1214+
audioCodec &&
1215+
audioCodec.indexOf('mp4a.40.5') !== -1 &&
1216+
ua.indexOf('android') !== -1 &&
1217+
audio.container !== 'audio/mpeg'
1218+
) {
12141219
// Exclude mpeg audio
12151220
audioCodec = 'mp4a.40.2';
12161221
this.log(`Android: force audio codec to ${audioCodec}`);

src/demux/transmuxer-interface.ts

+7
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import { ErrorTypes, ErrorDetails } from '../errors';
1010
import { getMediaSource } from '../utils/mediasource-helper';
1111
import { EventEmitter } from 'eventemitter3';
1212
import { Fragment, Part } from '../loader/fragment';
13+
import { getCodecCompatNames } from '../utils/codecs';
1314
import type { ChunkMetadata, TransmuxerResult } from '../types/transmuxer';
1415
import type Hls from '../hls';
1516
import type { HlsEventEmitter } from '../events';
1617
import type { PlaylistLevelType } from '../types/loader';
1718
import type { TypeSupported } from './tsdemuxer';
19+
import type { CodecCompatNames } from '../utils/codecs';
1820

1921
const MediaSource = getMediaSource() || { isTypeSupported: () => false };
2022

@@ -61,6 +63,8 @@ export default class TransmuxerInterface {
6163
mpeg: MediaSource.isTypeSupported('audio/mpeg'),
6264
mp3: MediaSource.isTypeSupported('audio/mp4; codecs="mp3"'),
6365
};
66+
const codecCompatNames: CodecCompatNames = getCodecCompatNames();
67+
6468
// navigator.vendor is not always available in Web Worker
6569
// refer to https://developer.mozilla.org/en-US/docs/Web/API/WorkerGlobalScope/navigator
6670
const vendor = navigator.vendor;
@@ -89,6 +93,7 @@ export default class TransmuxerInterface {
8993
worker.postMessage({
9094
cmd: 'init',
9195
typeSupported: typeSupported,
96+
codecCompatNames: codecCompatNames,
9297
vendor: vendor,
9398
id: id,
9499
config: JSON.stringify(config),
@@ -105,6 +110,7 @@ export default class TransmuxerInterface {
105110
this.transmuxer = new Transmuxer(
106111
this.observer,
107112
typeSupported,
113+
codecCompatNames,
108114
config,
109115
vendor,
110116
id
@@ -115,6 +121,7 @@ export default class TransmuxerInterface {
115121
this.transmuxer = new Transmuxer(
116122
this.observer,
117123
typeSupported,
124+
codecCompatNames,
118125
config,
119126
vendor,
120127
id

src/demux/transmuxer-worker.ts

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export default function TransmuxerWorker(self) {
3737
self.transmuxer = new Transmuxer(
3838
observer,
3939
data.typeSupported,
40+
data.codecCompatNames,
4041
config,
4142
data.vendor,
4243
data.id

src/demux/transmuxer.ts

+12-2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import type { TransmuxerResult, ChunkMetadata } from '../types/transmuxer';
1515
import type { HlsConfig } from '../config';
1616
import type { LevelKey } from '../loader/level-key';
1717
import type { PlaylistLevelType } from '../types/loader';
18+
import type { CodecCompatNames } from '../utils/codecs';
1819

1920
let now;
2021
// performance.now() not available on WebWorker, at least on Safari Desktop
@@ -41,6 +42,7 @@ const muxConfig: MuxConfig[] = [
4142
export default class Transmuxer {
4243
private observer: HlsEventEmitter;
4344
private typeSupported: TypeSupported;
45+
private codecCompatNames: CodecCompatNames;
4446
private config: HlsConfig;
4547
private vendor: string;
4648
private id: PlaylistLevelType;
@@ -55,12 +57,14 @@ export default class Transmuxer {
5557
constructor(
5658
observer: HlsEventEmitter,
5759
typeSupported: TypeSupported,
60+
codecCompatNames: CodecCompatNames,
5861
config: HlsConfig,
5962
vendor: string,
6063
id: PlaylistLevelType
6164
) {
6265
this.observer = observer;
6366
this.typeSupported = typeSupported;
67+
this.codecCompatNames = codecCompatNames;
6468
this.config = config;
6569
this.vendor = vendor;
6670
this.id = id;
@@ -392,7 +396,7 @@ export default class Transmuxer {
392396
data: Uint8Array,
393397
transmuxConfig: TransmuxConfig
394398
) {
395-
const { config, observer, typeSupported, vendor } = this;
399+
const { config, observer, typeSupported, codecCompatNames, vendor } = this;
396400
const {
397401
audioCodec,
398402
defaultInitPts,
@@ -421,7 +425,13 @@ export default class Transmuxer {
421425
const Remuxer: MuxConfig['remux'] = mux.remux;
422426
const Demuxer: MuxConfig['demux'] = mux.demux;
423427
if (!remuxer || !(remuxer instanceof Remuxer)) {
424-
this.remuxer = new Remuxer(observer, config, typeSupported, vendor);
428+
this.remuxer = new Remuxer(
429+
observer,
430+
config,
431+
typeSupported,
432+
codecCompatNames,
433+
vendor
434+
);
425435
}
426436
if (!demuxer || !(demuxer instanceof Demuxer)) {
427437
this.demuxer = new Demuxer(observer, config, typeSupported);

src/remux/mp4-remuxer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export default class MP4Remuxer implements Remuxer {
5151
observer: HlsEventEmitter,
5252
config: HlsConfig,
5353
typeSupported,
54+
codecCompatNames,
5455
vendor = ''
5556
) {
5657
this.observer = observer;

src/remux/passthrough-remuxer.ts

+20
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import type {
2424
DemuxedUserdataTrack,
2525
PassthroughTrack,
2626
} from '../types/demuxer';
27+
import type { CodecCompatNames } from '../utils/codecs';
2728

2829
class PassThroughRemuxer implements Remuxer {
2930
private emitInitSegment: boolean = false;
@@ -33,6 +34,17 @@ class PassThroughRemuxer implements Remuxer {
3334
private initPTS?: number;
3435
private initTracks?: TrackSet;
3536
private lastEndTime: number | null = null;
37+
private codecCompatNames: CodecCompatNames;
38+
39+
constructor(
40+
observer,
41+
config,
42+
typeSupported,
43+
codecCompatNames: CodecCompatNames,
44+
vendor
45+
) {
46+
this.codecCompatNames = codecCompatNames;
47+
}
3648

3749
public destroy() {}
3850

@@ -71,13 +83,19 @@ class PassThroughRemuxer implements Remuxer {
7183
initData.audio,
7284
ElementaryStreamTypes.AUDIO
7385
);
86+
if (audioCodec in this.codecCompatNames.audio) {
87+
audioCodec = this.codecCompatNames.audio[audioCodec];
88+
}
7489
}
7590

7691
if (!videoCodec) {
7792
videoCodec = getParsedTrackCodec(
7893
initData.video,
7994
ElementaryStreamTypes.VIDEO
8095
);
96+
if (videoCodec in this.codecCompatNames.video) {
97+
videoCodec = this.codecCompatNames.video[videoCodec];
98+
}
8199
}
82100

83101
const tracks: TrackSet = {};
@@ -246,6 +264,8 @@ function getParsedTrackCodec(
246264
if (parsedCodec === 'avc1' || type === ElementaryStreamTypes.VIDEO) {
247265
return 'avc1.42e01e';
248266
}
267+
if (parsedCodec === 'fLaC') return parsedCodec;
268+
if (parsedCodec === 'Opus') return parsedCodec;
249269
return 'mp4a.40.5';
250270
}
251271
export default PassThroughRemuxer;

src/utils/codecs.ts

+65
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ const sampleEntryCodesISO = {
1414
dtsh: true,
1515
'ec-3': true,
1616
enca: true,
17+
fLaC: true,
18+
flac: true, // non-compliant: browsers may expect this to be lowercase instead of MP4RA 'fLaC'
19+
FLAC: true, // non-compliant: apple tools recommended string
1720
g719: true,
1821
g726: true,
1922
m4ae: true,
@@ -83,3 +86,65 @@ export function isCodecSupportedInMp4(codec: string, type: CodecType): boolean {
8386
`${type || 'video'}/mp4;codecs="${codec}"`
8487
);
8588
}
89+
90+
interface AudioCodecCompatNames {
91+
flac?: string;
92+
fLaC?: string;
93+
FLAC?: string;
94+
Opus?: string;
95+
opus?: string;
96+
}
97+
98+
export interface CodecCompatNames {
99+
audio: AudioCodecCompatNames;
100+
video: {};
101+
text: {};
102+
}
103+
104+
// Some browsers expect the codec strings for FLAC and Opus
105+
// to be lowercase. The MP4RA defines the codec values as
106+
// "fLaC" and "Opus", but most browsers supported the lowercase
107+
// strings before the MP4RA published these values.
108+
//
109+
// Additionally, some Apple tools recommended the string
110+
// "FLAC" in manifest files, which is also non-compliant. We use
111+
// this to re-map codec names as necessary.
112+
113+
export function getCodecCompatNames(): CodecCompatNames {
114+
const audioCompatNames: AudioCodecCompatNames = {};
115+
const videoCompatNames = {};
116+
const textCompatNames = {};
117+
118+
// if the MP4RA string is available, opt for that
119+
// in all cases
120+
if (isCodecSupportedInMp4('fLaC', 'audio')) {
121+
audioCompatNames.FLAC = 'fLaC';
122+
audioCompatNames.flac = 'fLaC';
123+
} else {
124+
if (isCodecSupportedInMp4('flac', 'audio')) {
125+
audioCompatNames.fLaC = 'flac';
126+
audioCompatNames.FLAC = 'flac';
127+
} else {
128+
if (isCodecSupportedInMp4('FLAC', 'audio')) {
129+
audioCompatNames.flac = 'FLAC';
130+
audioCompatNames.fLaC = 'FLAC';
131+
}
132+
}
133+
}
134+
135+
if (isCodecSupportedInMp4('Opus', 'audio')) {
136+
audioCompatNames.opus = 'Opus';
137+
} else {
138+
if (isCodecSupportedInMp4('opus', 'audio')) {
139+
audioCompatNames.Opus = 'opus';
140+
}
141+
}
142+
143+
const codecCompatNames: CodecCompatNames = {
144+
audio: audioCompatNames,
145+
video: videoCompatNames,
146+
text: textCompatNames,
147+
};
148+
149+
return codecCompatNames;
150+
}

0 commit comments

Comments
 (0)