mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-02 13:23:51 +02:00
Fix HLS demuxer parsing bug, extract HLS mime type out, add TargetRequest.mimeType
This commit is contained in:
+12
-4
@@ -20,7 +20,7 @@ import { TrackType } from '../output';
|
|||||||
import { assert, joinPaths, MaybePromise, Rotation, UNDETERMINED_LANGUAGE } from '../misc';
|
import { assert, joinPaths, MaybePromise, Rotation, UNDETERMINED_LANGUAGE } from '../misc';
|
||||||
import { EncodedPacket } from '../packet';
|
import { EncodedPacket } from '../packet';
|
||||||
import { readAllLines } from '../reader';
|
import { readAllLines } from '../reader';
|
||||||
import { AttributeList, canIgnoreLine } from './hls-misc';
|
import { AttributeList, canIgnoreLine, HLS_MIME_TYPE } from './hls-misc';
|
||||||
import { HlsSegmentedInput } from './hls-segmented-input';
|
import { HlsSegmentedInput } from './hls-segmented-input';
|
||||||
import { PathedSource } from '../source';
|
import { PathedSource } from '../source';
|
||||||
import { SegmentedInputTrackDeclaration } from '../segmented-input';
|
import { SegmentedInputTrackDeclaration } from '../segmented-input';
|
||||||
@@ -105,7 +105,7 @@ export class HlsDemuxer extends Demuxer {
|
|||||||
if (bandwidth === null) {
|
if (bandwidth === null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Invalid M3U8 file; #EXT-X-STREAM-INF tag requires a BANDWIDTH attribute with a valid'
|
'Invalid M3U8 file; #EXT-X-STREAM-INF tag requires a BANDWIDTH attribute with a valid'
|
||||||
+ ' number value.',
|
+ ' numerical value.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ export class HlsDemuxer extends Demuxer {
|
|||||||
hasOnlyKeyPackets: false,
|
hasOnlyKeyPackets: false,
|
||||||
});
|
});
|
||||||
} else if (line.startsWith('#EXT-X-I-FRAME-STREAM-INF:')) {
|
} else if (line.startsWith('#EXT-X-I-FRAME-STREAM-INF:')) {
|
||||||
const attributes = new AttributeList(line.slice(18));
|
const attributes = new AttributeList(line.slice(26));
|
||||||
const playlistPath = attributes.get('uri');
|
const playlistPath = attributes.get('uri');
|
||||||
|
|
||||||
if (playlistPath === null) {
|
if (playlistPath === null) {
|
||||||
@@ -125,6 +125,14 @@ export class HlsDemuxer extends Demuxer {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bandwidth = attributes.getAsNumber('bandwidth');
|
||||||
|
if (bandwidth === null) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid M3U8 file; #EXT-X-I-FRAME-STREAM-INF tag requires a BANDWIDTH attribute with a'
|
||||||
|
+ ' valid numerical value.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const fullPath = joinPaths(source.rootPath, playlistPath);
|
const fullPath = joinPaths(source.rootPath, playlistPath);
|
||||||
|
|
||||||
variantStreams.push({
|
variantStreams.push({
|
||||||
@@ -610,7 +618,7 @@ export class HlsDemuxer extends Demuxer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getMimeType(): Promise<string> {
|
async getMimeType(): Promise<string> {
|
||||||
return 'application/vnd.apple.mpegurl';
|
return HLS_MIME_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
override dispose(): void {
|
override dispose(): void {
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
export const HLS_MIME_TYPE = 'application/vnd.apple.mpegurl';
|
||||||
|
|
||||||
export const canIgnoreLine = (line: string) => line.length === 0 || (line.startsWith('#') && !line.startsWith('#EXT'));
|
export const canIgnoreLine = (line: string) => line.length === 0 || (line.startsWith('#') && !line.startsWith('#EXT'));
|
||||||
|
|
||||||
export class AttributeList {
|
export class AttributeList {
|
||||||
|
|||||||
+18
-4
@@ -29,6 +29,7 @@ import { Writer } from '../writer';
|
|||||||
import { EncodedPacket } from '../packet';
|
import { EncodedPacket } from '../packet';
|
||||||
import { SubtitleCue, SubtitleMetadata } from '../subtitles';
|
import { SubtitleCue, SubtitleMetadata } from '../subtitles';
|
||||||
import { NullTarget, PathedTarget, Target, TargetRequest } from '../target';
|
import { NullTarget, PathedTarget, Target, TargetRequest } from '../target';
|
||||||
|
import { HLS_MIME_TYPE } from './hls-misc';
|
||||||
|
|
||||||
type HlsTrackData = {
|
type HlsTrackData = {
|
||||||
track: OutputTrack;
|
track: OutputTrack;
|
||||||
@@ -499,7 +500,7 @@ export class HlsMuxer extends Muxer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getMimeType(): Promise<string> {
|
async getMimeType(): Promise<string> {
|
||||||
return 'application/vnd.apple.mpegurl';
|
return HLS_MIME_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private allTracksAreKnown(playlist: Playlist) {
|
private allTracksAreKnown(playlist: Playlist) {
|
||||||
@@ -829,7 +830,11 @@ export class HlsMuxer extends Muxer {
|
|||||||
relativeSegmentPath,
|
relativeSegmentPath,
|
||||||
);
|
);
|
||||||
|
|
||||||
const target = await this.output._getTarget({ path: fullSegmentPath, isRoot: false });
|
const target = await this.output._getTarget({
|
||||||
|
path: fullSegmentPath,
|
||||||
|
isRoot: false,
|
||||||
|
mimeType: playlist.segmentFormat.mimeType,
|
||||||
|
});
|
||||||
target._start();
|
target._start();
|
||||||
|
|
||||||
playlist.singleFile = {
|
playlist.singleFile = {
|
||||||
@@ -926,6 +931,7 @@ export class HlsMuxer extends Muxer {
|
|||||||
const target = await this.output._getTarget({
|
const target = await this.output._getTarget({
|
||||||
path: initPath,
|
path: initPath,
|
||||||
isRoot: false,
|
isRoot: false,
|
||||||
|
mimeType: playlist.segmentFormat.mimeType,
|
||||||
});
|
});
|
||||||
target.on('write', ({ end }) => {
|
target.on('write', ({ end }) => {
|
||||||
playlist.initSegment!.byteSize = Math.max(playlist.initSegment!.byteSize, end);
|
playlist.initSegment!.byteSize = Math.max(playlist.initSegment!.byteSize, end);
|
||||||
@@ -1179,7 +1185,11 @@ export class HlsMuxer extends Muxer {
|
|||||||
|
|
||||||
this.format._options.onPlaylist?.(playlistText, toPlaylistInfo(playlist));
|
this.format._options.onPlaylist?.(playlistText, toPlaylistInfo(playlist));
|
||||||
|
|
||||||
const target = await this.output._getTarget({ path: playlistPath, isRoot: false });
|
const target = await this.output._getTarget({
|
||||||
|
path: playlistPath,
|
||||||
|
isRoot: false,
|
||||||
|
mimeType: HLS_MIME_TYPE,
|
||||||
|
});
|
||||||
const writer = new Writer(target);
|
const writer = new Writer(target);
|
||||||
writer.start();
|
writer.start();
|
||||||
writer.write(textEncoder.encode(playlistText));
|
writer.write(textEncoder.encode(playlistText));
|
||||||
@@ -1382,7 +1392,11 @@ export class HlsMuxer extends Muxer {
|
|||||||
} else {
|
} else {
|
||||||
// For subsequent master playlist writes, we *must* obtain a different target in order to overwrite
|
// For subsequent master playlist writes, we *must* obtain a different target in order to overwrite
|
||||||
// the file.
|
// the file.
|
||||||
const target = await this.output._getTarget({ path: pathedTarget.rootPath, isRoot: true });
|
const target = await this.output._getTarget({
|
||||||
|
path: pathedTarget.rootPath,
|
||||||
|
isRoot: true,
|
||||||
|
mimeType: HLS_MIME_TYPE,
|
||||||
|
});
|
||||||
writer = new Writer(target);
|
writer = new Writer(target);
|
||||||
writer.start();
|
writer.start();
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-1
@@ -33,6 +33,7 @@ import { FlacDemuxer } from './flac/flac-demuxer';
|
|||||||
import { MpegTsDemuxer } from './mpeg-ts/mpeg-ts-demuxer';
|
import { MpegTsDemuxer } from './mpeg-ts/mpeg-ts-demuxer';
|
||||||
import { TS_PACKET_SIZE } from './mpeg-ts/mpeg-ts-misc';
|
import { TS_PACKET_SIZE } from './mpeg-ts/mpeg-ts-misc';
|
||||||
import { HlsDemuxer } from './hls/hls-demuxer';
|
import { HlsDemuxer } from './hls/hls-demuxer';
|
||||||
|
import { HLS_MIME_TYPE } from './hls/hls-misc';
|
||||||
import { PathedSource } from './source';
|
import { PathedSource } from './source';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -610,7 +611,7 @@ export class HlsInputFormat extends InputFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get mimeType() {
|
get mimeType() {
|
||||||
return 'application/vnd.apple.mpegurl';
|
return HLS_MIME_TYPE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { Output, OutputTrack, TrackType } from './output';
|
|||||||
import { MpegTsMuxer } from './mpeg-ts/mpeg-ts-muxer';
|
import { MpegTsMuxer } from './mpeg-ts/mpeg-ts-muxer';
|
||||||
import { WaveMuxer } from './wave/wave-muxer';
|
import { WaveMuxer } from './wave/wave-muxer';
|
||||||
import { HlsMuxer } from './hls/hls-muxer';
|
import { HlsMuxer } from './hls/hls-muxer';
|
||||||
|
import { HLS_MIME_TYPE } from './hls/hls-misc';
|
||||||
import { MaybePromise, FilePath, toArray } from './misc';
|
import { MaybePromise, FilePath, toArray } from './misc';
|
||||||
import { Target } from './target';
|
import { Target } from './target';
|
||||||
|
|
||||||
@@ -1368,7 +1369,7 @@ export class HlsOutputFormat extends OutputFormat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get mimeType() {
|
get mimeType() {
|
||||||
return 'application/vnd.apple.mpegurl';
|
return HLS_MIME_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
getSupportedCodecs(): MediaCodec[] {
|
getSupportedCodecs(): MediaCodec[] {
|
||||||
|
|||||||
+5
-1
@@ -534,7 +534,11 @@ export class Output<
|
|||||||
return this._target;
|
return this._target;
|
||||||
}
|
}
|
||||||
|
|
||||||
const request: TargetRequest = { path: this._target.rootPath, isRoot: true };
|
const request: TargetRequest = {
|
||||||
|
path: this._target.rootPath,
|
||||||
|
isRoot: true,
|
||||||
|
mimeType: this.format.mimeType,
|
||||||
|
};
|
||||||
const result = this._getTargetValidated(request);
|
const result = this._getTargetValidated(request);
|
||||||
|
|
||||||
const handleResult = (target: T) => {
|
const handleResult = (target: T) => {
|
||||||
|
|||||||
+3
-1
@@ -727,6 +727,8 @@ export class PathedTarget<T extends Target> {
|
|||||||
export type TargetRequest = {
|
export type TargetRequest = {
|
||||||
/** The requested file path. */
|
/** The requested file path. */
|
||||||
path: FilePath;
|
path: FilePath;
|
||||||
/** Whether the requested file is the root file. */
|
/** Whether the to-be-written file will be the root file. */
|
||||||
isRoot: boolean;
|
isRoot: boolean;
|
||||||
|
/** The MIME type of the to-be-written file. */
|
||||||
|
mimeType: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @stylistic/max-len */
|
/* eslint-disable @stylistic/max-len */
|
||||||
import { ALL_FORMATS, createInputFrom, EncodedPacketSink, InputAudioTrack, InputVideoTrack } from 'mediabunny';
|
import { ALL_FORMATS, BufferSource, createInputFrom, EncodedPacketSink, Input, InputAudioTrack, InputVideoTrack, PathedSource } from 'mediabunny';
|
||||||
import { expect, test } from 'vitest';
|
import { expect, test } from 'vitest';
|
||||||
import { HLS, HlsInputFormat } from '../../src/input-format.js';
|
import { HLS, HlsInputFormat } from '../../src/input-format.js';
|
||||||
import { assert, rejectAfter } from '../../src/misc.js';
|
import { assert, rejectAfter } from '../../src/misc.js';
|
||||||
@@ -721,3 +721,45 @@ test.concurrent('Live HLS', { timeout: 30_000 }, async () => {
|
|||||||
]);
|
]);
|
||||||
expect(metadataDuration).toBeGreaterThan((Date.now() / 1000) - 3600);
|
expect(metadataDuration).toBeGreaterThan((Date.now() / 1000) - 3600);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.concurrent('#EXT-X-I-FRAME-STREAM-INF tags are parsed properly', async () => {
|
||||||
|
const text = `#EXTM3U
|
||||||
|
#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=256000,CODECS="avc1.4D401E",RESOLUTION=480x270,URI="7f2459cb12854fdbbc7ec1e7279da179/f74fb10563564130a4702743d64112a3/index_11.m3u8"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const input = new Input({
|
||||||
|
formats: ALL_FORMATS,
|
||||||
|
source: new PathedSource('master.m3u8', () => new BufferSource(new TextEncoder().encode(text))),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tracks = await input.getTracks();
|
||||||
|
expect(tracks).toHaveLength(1);
|
||||||
|
expect(await tracks[0]!.hasOnlyKeyPackets()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('#EXT-X-I-FRAME-STREAM-INF tags without BANDWIDTH attribute are rejected', async () => {
|
||||||
|
const text = `#EXTM3U
|
||||||
|
#EXT-X-I-FRAME-STREAM-INF:CODECS="avc1.4D401E",RESOLUTION=480x270,URI="7f2459cb12854fdbbc7ec1e7279da179/f74fb10563564130a4702743d64112a3/index_11.m3u8"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const input = new Input({
|
||||||
|
formats: ALL_FORMATS,
|
||||||
|
source: new PathedSource('master.m3u8', () => new BufferSource(new TextEncoder().encode(text))),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(input.getTracks()).rejects.toThrow('BANDWIDTH');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.concurrent('#EXT-X-STREAM-INF tags without BANDWIDTH attribute are rejected', async () => {
|
||||||
|
const text = `#EXTM3U
|
||||||
|
#EXT-X-STREAM-INF:CODECS="avc1.4D401E",RESOLUTION=480x270
|
||||||
|
7f2459cb12854fdbbc7ec1e7279da179/f74fb10563564130a4702743d64112a3/index_11.m3u8
|
||||||
|
`;
|
||||||
|
|
||||||
|
const input = new Input({
|
||||||
|
formats: ALL_FORMATS,
|
||||||
|
source: new PathedSource('master.m3u8', () => new BufferSource(new TextEncoder().encode(text))),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(input.getTracks()).rejects.toThrow('BANDWIDTH');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user