mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-10-03 13:53:53 +02:00
Fix HLS muxer audio segmentation bug, fix conversion tests failing in CI
This commit is contained in:
@@ -691,6 +691,7 @@ export class HlsMuxer extends Muxer {
|
|||||||
let audioEndIndex = 0;
|
let audioEndIndex = 0;
|
||||||
|
|
||||||
if (videoTrack && (!videoTrack.closed || videoTrack.packets.length > 0)) {
|
if (videoTrack && (!videoTrack.closed || videoTrack.packets.length > 0)) {
|
||||||
|
// A video track is active (and maybe an audio track too)
|
||||||
const allBelow = videoTrack.packets.every(x => x.timestamp < currentSegmentEndTimestamp);
|
const allBelow = videoTrack.packets.every(x => x.timestamp < currentSegmentEndTimestamp);
|
||||||
|
|
||||||
let bestKeyPacket: EncodedPacket | null = null;
|
let bestKeyPacket: EncodedPacket | null = null;
|
||||||
@@ -773,21 +774,22 @@ export class HlsMuxer extends Muxer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (audioTrack && (!audioTrack.closed || audioTrack.packets.length > 0)) {
|
} else if (audioTrack && (!audioTrack.closed || audioTrack.packets.length > 0)) {
|
||||||
|
// There's only an audio track active
|
||||||
|
|
||||||
const allBelow = audioTrack.packets.every(x => x.timestamp < currentSegmentEndTimestamp);
|
const allBelow = audioTrack.packets.every(x => x.timestamp < currentSegmentEndTimestamp);
|
||||||
|
|
||||||
if (allBelow) {
|
if (allBelow) {
|
||||||
if (audioTrack.closed) {
|
if (audioTrack.closed) {
|
||||||
|
// We can write all packets since they're all below
|
||||||
audioEndIndex = audioTrack.packets.length;
|
audioEndIndex = audioTrack.packets.length;
|
||||||
} else {
|
} else {
|
||||||
|
// We don't know enough packets yet
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// Aim to make the segment at most as long as desired
|
||||||
const index = findLastIndex(audioTrack.packets, x => x.timestamp <= currentSegmentEndTimestamp);
|
const index = findLastIndex(audioTrack.packets, x => x.timestamp <= currentSegmentEndTimestamp);
|
||||||
if (index !== -1) {
|
audioEndIndex = Math.max(index, 1); // Always include at least the first packet
|
||||||
audioEndIndex = index;
|
|
||||||
} else {
|
|
||||||
audioEndIndex = 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -72,7 +72,7 @@ export abstract class Target extends EventEmitter<TargetEvents> {
|
|||||||
this._emit('write', { start, end });
|
this._emit('write', { start, end });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a new {@link RangedTarget} that writes data to this target using the given offset.
|
* Returns a new {@link RangedTarget} that writes data to this target using the given offset.
|
||||||
*
|
*
|
||||||
* Useful for writing a file into a section of a larger file.
|
* Useful for writing a file into a section of a larger file.
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import { BufferTarget, PathedTarget } from '../../src/target.js';
|
|||||||
import { Conversion } from '../../src/conversion.js';
|
import { Conversion } from '../../src/conversion.js';
|
||||||
import { assert } from '../../src/misc.js';
|
import { assert } from '../../src/misc.js';
|
||||||
import { InputVideoTrack } from '../../src/input-track.js';
|
import { InputVideoTrack } from '../../src/input-track.js';
|
||||||
import { AudioBufferSource, CanvasSource } from '../../src/media-source.js';
|
import { CanvasSource, EncodedAudioPacketSource } from '../../src/media-source.js';
|
||||||
import { QUALITY_HIGH } from '../../src/encode.js';
|
import { QUALITY_HIGH } from '../../src/encode.js';
|
||||||
|
import { EncodedPacket } from '../../src/packet.js';
|
||||||
|
|
||||||
test('Rotation is baked-in when rerendering', async () => {
|
test('Rotation is baked in when rerendering', async () => {
|
||||||
using input = new Input({
|
using input = new Input({
|
||||||
source: new UrlSource('/rotate-buck-bunny.mp4'),
|
source: new UrlSource('/rotate-buck-bunny.mp4'),
|
||||||
formats: ALL_FORMATS,
|
formats: ALL_FORMATS,
|
||||||
@@ -62,7 +63,7 @@ test('Exceeding max allowed track count', async () => {
|
|||||||
target: new BufferTarget(),
|
target: new BufferTarget(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const conversion = await Conversion.init({ input, output });
|
const conversion = await Conversion.init({ input, output, showWarnings: false });
|
||||||
expect(conversion.utilizedTracks).toHaveLength(1);
|
expect(conversion.utilizedTracks).toHaveLength(1);
|
||||||
expect(conversion.discardedTracks).toHaveLength(1);
|
expect(conversion.discardedTracks).toHaveLength(1);
|
||||||
expect(conversion.discardedTracks[0]!.reason).toBe('max_track_count_reached');
|
expect(conversion.discardedTracks[0]!.reason).toBe('max_track_count_reached');
|
||||||
@@ -84,6 +85,7 @@ test('Fan-out', async () => {
|
|||||||
output,
|
output,
|
||||||
video: [{ height: 480 }, { height: 360 }],
|
video: [{ height: 480 }, { height: 360 }],
|
||||||
audio: [], // Identical to discarding it
|
audio: [], // Identical to discarding it
|
||||||
|
showWarnings: false,
|
||||||
});
|
});
|
||||||
expect(conversion.utilizedTracks).toHaveLength(2);
|
expect(conversion.utilizedTracks).toHaveLength(2);
|
||||||
expect(conversion.discardedTracks).toHaveLength(1);
|
expect(conversion.discardedTracks).toHaveLength(1);
|
||||||
@@ -103,21 +105,25 @@ test('Fan-out', async () => {
|
|||||||
expect(await tracks[1]!.getDisplayHeight()).toBe(360);
|
expect(await tracks[1]!.getDisplayHeight()).toBe(360);
|
||||||
});
|
});
|
||||||
|
|
||||||
const createSineWave = (sampleRate: number, channels: number, durationSeconds: number) => {
|
// eslint-disable-next-line @stylistic/max-len
|
||||||
const buffer = new AudioBuffer({
|
const aacPacketData = new Uint8Array([255, 241, 77, 128, 3, 159, 252, 0, 208, 0, 1, 3, 64, 0, 13, 0, 0, 17, 52, 0, 0, 208, 0, 3, 6, 128, 0, 56]);
|
||||||
sampleRate,
|
const aacMetadata: EncodedAudioChunkMetadata = {
|
||||||
numberOfChannels: channels,
|
decoderConfig: {
|
||||||
length: sampleRate * durationSeconds,
|
codec: 'mp4a.40.2',
|
||||||
});
|
numberOfChannels: 2,
|
||||||
|
sampleRate: 48000,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
for (let ch = 0; ch < channels; ch++) {
|
const addAacPackets = async (source: EncodedAudioPacketSource, durationSeconds: number) => {
|
||||||
const data = buffer.getChannelData(ch);
|
const packetDuration = 1024 / 48000;
|
||||||
for (let i = 0; i < data.length; i++) {
|
const count = Math.ceil(durationSeconds / packetDuration);
|
||||||
data[i] = Math.sin(2 * Math.PI * 440 * i / sampleRate);
|
for (let i = 0; i < count; i++) {
|
||||||
}
|
await source.add(
|
||||||
|
new EncodedPacket(aacPacketData, 'key', i * packetDuration, packetDuration),
|
||||||
|
i === 0 ? aacMetadata : undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return buffer;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('HLS track assignability is kept #1', async () => {
|
test('HLS track assignability is kept #1', async () => {
|
||||||
@@ -148,7 +154,7 @@ test('HLS track assignability is kept #1', async () => {
|
|||||||
const videoSource = new CanvasSource(canvas, { codec: 'avc', bitrate: QUALITY_HIGH });
|
const videoSource = new CanvasSource(canvas, { codec: 'avc', bitrate: QUALITY_HIGH });
|
||||||
output.addVideoTrack(videoSource);
|
output.addVideoTrack(videoSource);
|
||||||
|
|
||||||
const audioSource = new AudioBufferSource({ codec: 'aac', bitrate: QUALITY_HIGH });
|
const audioSource = new EncodedAudioPacketSource('aac');
|
||||||
output.addAudioTrack(audioSource);
|
output.addAudioTrack(audioSource);
|
||||||
|
|
||||||
await output.start();
|
await output.start();
|
||||||
@@ -157,7 +163,7 @@ test('HLS track assignability is kept #1', async () => {
|
|||||||
await videoSource.add(i / 2, 1 / 2);
|
await videoSource.add(i / 2, 1 / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
await audioSource.add(createSineWave(48000, 2, 2));
|
await addAacPackets(audioSource, 2);
|
||||||
|
|
||||||
await output.finalize();
|
await output.finalize();
|
||||||
|
|
||||||
@@ -227,7 +233,7 @@ test('HLS track assignability is kept #2', async () => {
|
|||||||
const videoSource = new CanvasSource(canvas, { codec: 'avc', bitrate: QUALITY_HIGH });
|
const videoSource = new CanvasSource(canvas, { codec: 'avc', bitrate: QUALITY_HIGH });
|
||||||
output.addVideoTrack(videoSource, { group: a });
|
output.addVideoTrack(videoSource, { group: a });
|
||||||
|
|
||||||
const audioSource = new AudioBufferSource({ codec: 'aac', bitrate: QUALITY_HIGH });
|
const audioSource = new EncodedAudioPacketSource('aac');
|
||||||
output.addAudioTrack(audioSource, { group: b });
|
output.addAudioTrack(audioSource, { group: b });
|
||||||
|
|
||||||
await output.start();
|
await output.start();
|
||||||
@@ -236,7 +242,7 @@ test('HLS track assignability is kept #2', async () => {
|
|||||||
await videoSource.add(i / 2, 1 / 2);
|
await videoSource.add(i / 2, 1 / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
await audioSource.add(createSineWave(48000, 2, 2));
|
await addAacPackets(audioSource, 2);
|
||||||
|
|
||||||
await output.finalize();
|
await output.finalize();
|
||||||
|
|
||||||
@@ -306,7 +312,7 @@ test('HLS track assignability can be overridden', async () => {
|
|||||||
const videoSource = new CanvasSource(canvas, { codec: 'avc', bitrate: QUALITY_HIGH });
|
const videoSource = new CanvasSource(canvas, { codec: 'avc', bitrate: QUALITY_HIGH });
|
||||||
output.addVideoTrack(videoSource, { group: a });
|
output.addVideoTrack(videoSource, { group: a });
|
||||||
|
|
||||||
const audioSource = new AudioBufferSource({ codec: 'aac', bitrate: QUALITY_HIGH });
|
const audioSource = new EncodedAudioPacketSource('aac');
|
||||||
output.addAudioTrack(audioSource, { group: b });
|
output.addAudioTrack(audioSource, { group: b });
|
||||||
|
|
||||||
await output.start();
|
await output.start();
|
||||||
@@ -315,7 +321,7 @@ test('HLS track assignability can be overridden', async () => {
|
|||||||
await videoSource.add(i / 2, 1 / 2);
|
await videoSource.add(i / 2, 1 / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
await audioSource.add(createSineWave(48000, 2, 2));
|
await addAacPackets(audioSource, 2);
|
||||||
|
|
||||||
await output.finalize();
|
await output.finalize();
|
||||||
|
|
||||||
|
|||||||
@@ -1487,6 +1487,60 @@ segment-1-3.ts
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Segmentation, one video packet per segment', async () => {
|
||||||
|
const env = await setUpSegmentationEnvironment({ video: true });
|
||||||
|
|
||||||
|
await env.addVideoPacket(0, 'key', 3);
|
||||||
|
expect(env.segmentCount).toBe(0);
|
||||||
|
await env.addVideoPacket(3, 'key', 3);
|
||||||
|
expect(env.segmentCount).toBe(1);
|
||||||
|
|
||||||
|
await env.output.finalize();
|
||||||
|
expect(env.segmentCount).toBe(2);
|
||||||
|
|
||||||
|
expect(env.result).toBe(`#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
|
#EXT-X-TARGETDURATION:3
|
||||||
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
|
#EXTINF:3,
|
||||||
|
segment-1-1.ts
|
||||||
|
#EXTINF:3,
|
||||||
|
segment-1-2.ts
|
||||||
|
|
||||||
|
#EXT-X-ENDLIST
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Segmentation, one audio packet per segment', async () => {
|
||||||
|
const env = await setUpSegmentationEnvironment({ audio: true });
|
||||||
|
|
||||||
|
await env.addAudioPacket(0, 3);
|
||||||
|
expect(env.segmentCount).toBe(0);
|
||||||
|
await env.addAudioPacket(3, 3);
|
||||||
|
expect(env.segmentCount).toBe(1);
|
||||||
|
|
||||||
|
await env.output.finalize();
|
||||||
|
expect(env.segmentCount).toBe(2);
|
||||||
|
|
||||||
|
expect(env.result).toBe(`#EXTM3U
|
||||||
|
#EXT-X-VERSION:3
|
||||||
|
#EXT-X-PLAYLIST-TYPE:VOD
|
||||||
|
#EXT-X-TARGETDURATION:3
|
||||||
|
#EXT-X-INDEPENDENT-SEGMENTS
|
||||||
|
|
||||||
|
#EXTINF:3,
|
||||||
|
segment-1-1.ts
|
||||||
|
#EXTINF:3,
|
||||||
|
segment-1-2.ts
|
||||||
|
|
||||||
|
#EXT-X-ENDLIST
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('Segmentation, dual-track, single segment', async () => {
|
test('Segmentation, dual-track, single segment', async () => {
|
||||||
const env = await setUpSegmentationEnvironment({ video: true, audio: true });
|
const env = await setUpSegmentationEnvironment({ video: true, audio: true });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user