Skip to content

Commit 60f8146

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 replaces any instance of "flac" and "opus" (any case) with the first compatible codec string.
1 parent 50c6b16 commit 60f8146

File tree

5 files changed

+76
-4
lines changed

5 files changed

+76
-4
lines changed

src/controller/buffer-controller.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Events } from '../events';
22
import { logger } from '../utils/logger';
33
import { ErrorDetails, ErrorTypes } from '../errors';
44
import { BufferHelper } from '../utils/buffer-helper';
5+
import { getCodecCompatibleName } from '../utils/codecs';
56
import { getMediaSource } from '../utils/mediasource-helper';
67
import { ElementaryStreamTypes } from '../loader/fragment';
78
import type { TrackSet } from '../types/track';
@@ -29,6 +30,7 @@ import { LevelDetails } from '../loader/level-details';
2930

3031
const MediaSource = getMediaSource();
3132
const VIDEO_CODEC_PROFILE_REPACE = /([ha]vc.)(?:\.[^.,]+)+/;
33+
const AUDIO_CODEC_REGEXP = /flac|opus/gi;
3234

3335
export default class BufferController implements ComponentAPI {
3436
// The level details used to determine duration, target-duration and live
@@ -254,7 +256,14 @@ export default class BufferController implements ComponentAPI {
254256
'$1'
255257
);
256258
if (currentCodec !== nextCodec) {
257-
const mimeType = `${container};codecs=${levelCodec || codec}`;
259+
let trackCodec = levelCodec || codec;
260+
if (trackName.indexOf('audio') !== -1) {
261+
trackCodec = trackCodec.replace(
262+
AUDIO_CODEC_REGEXP,
263+
getCodecCompatibleName
264+
);
265+
}
266+
const mimeType = `${container};codecs=${trackCodec}`;
258267
this.appendChangeType(trackName, mimeType);
259268
logger.log(
260269
`[buffer-controller]: switching codec ${currentCodec} to ${nextCodec}`
@@ -712,7 +721,12 @@ export default class BufferController implements ComponentAPI {
712721
);
713722
}
714723
// use levelCodec as first priority
715-
const codec = track.levelCodec || track.codec;
724+
let codec = track.levelCodec || track.codec;
725+
if (codec) {
726+
if (trackName.indexOf('audio') !== -1) {
727+
codec = codec.replace(AUDIO_CODEC_REGEXP, getCodecCompatibleName);
728+
}
729+
}
716730
const mimeType = `${track.container};codecs=${codec}`;
717731
logger.log(`[buffer-controller]: creating sourceBuffer(${mimeType})`);
718732
try {

src/controller/level-controller.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import { Level } from '../types/level';
1515
import { Events } from '../events';
1616
import { ErrorTypes, ErrorDetails } from '../errors';
17-
import { isCodecSupportedInMp4 } from '../utils/codecs';
17+
import { isCodecSupportedInMp4, getCodecCompatibleName } from '../utils/codecs';
1818
import { addGroupId, assignTrackIdsByGroup } from './level-helper';
1919
import BasePlaylistController from './base-playlist-controller';
2020
import { PlaylistContextType, PlaylistLevelType } from '../types/loader';
@@ -26,6 +26,8 @@ const chromeOrFirefox: boolean = /chrome|firefox/.test(
2626
navigator.userAgent.toLowerCase()
2727
);
2828

29+
const AUDIO_CODEC_REGEXP = /flac|opus/gi;
30+
2931
export default class LevelController extends BasePlaylistController {
3032
private _levels: Level[] = [];
3133
private _firstLevel: number = -1;
@@ -109,6 +111,13 @@ export default class LevelController extends BasePlaylistController {
109111
levelParsed.audioCodec = undefined;
110112
}
111113

114+
if (levelParsed.audioCodec) {
115+
levelParsed.audioCodec = levelParsed.audioCodec.replace(
116+
AUDIO_CODEC_REGEXP,
117+
getCodecCompatibleName
118+
);
119+
}
120+
112121
const levelKey = `${levelParsed.bitrate}-${levelParsed.attrs.RESOLUTION}-${levelParsed.attrs.CODECS}`;
113122
levelFromSet = levelSet[levelKey];
114123

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/remux/passthrough-remuxer.ts

+6
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,12 @@ function getParsedTrackCodec(
246246
if (parsedCodec === 'avc1' || type === ElementaryStreamTypes.VIDEO) {
247247
return 'avc1.42e01e';
248248
}
249+
if (parsedCodec === 'fLaC') {
250+
return 'fLaC';
251+
}
252+
if (parsedCodec === 'Opus') {
253+
return 'Opus';
254+
}
249255
return 'mp4a.40.5';
250256
}
251257
export default PassThroughRemuxer;

src/utils/codecs.ts

+38
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, // MP4-RA listed codec entry for FLAC
18+
flac: true, // legacy browser codec name for FLAC
19+
FLAC: true, // some manifests may list "FLAC" with Apple's tools
1720
g719: true,
1821
g726: true,
1922
m4ae: true,
@@ -83,3 +86,38 @@ export function isCodecSupportedInMp4(codec: string, type: CodecType): boolean {
8386
`${type || 'video'}/mp4;codecs="${codec}"`
8487
);
8588
}
89+
90+
interface CodecNameCache {
91+
flac?: string;
92+
opus?: string;
93+
}
94+
95+
const CODEC_COMPATIBLE_NAMES: CodecNameCache = {};
96+
97+
type LowerCaseCodecType = 'flac' | 'opus';
98+
99+
function getCodecCompatibleNameLower(
100+
lowerCaseCodec: LowerCaseCodecType
101+
): string {
102+
if (CODEC_COMPATIBLE_NAMES[lowerCaseCodec]) {
103+
return CODEC_COMPATIBLE_NAMES[lowerCaseCodec]!;
104+
}
105+
106+
const codecsToCheck = {
107+
flac: ['fLaC', 'flac', 'FLAC'],
108+
opus: ['Opus', 'opus'],
109+
}[lowerCaseCodec];
110+
111+
for (let i = 0; i < codecsToCheck.length; i++) {
112+
if (isCodecSupportedInMp4(codecsToCheck[i], 'audio')) {
113+
CODEC_COMPATIBLE_NAMES[lowerCaseCodec] = codecsToCheck[i];
114+
return codecsToCheck[i];
115+
}
116+
}
117+
118+
return lowerCaseCodec;
119+
}
120+
121+
export function getCodecCompatibleName(codec: string): string {
122+
return getCodecCompatibleNameLower(codec.toLowerCase() as LowerCaseCodecType);
123+
}

0 commit comments

Comments
 (0)