mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-26 07:45:34 +00:00
add audio passthrough export path
This commit is contained in:
@@ -12,6 +12,70 @@ const MP4_AUDIO_CODEC = 'mp4a.40.2'
|
||||
export class AudioProcessor {
|
||||
private cancelled = false
|
||||
|
||||
private isPassthroughAudioCodec(codec: string | undefined): boolean {
|
||||
if (!codec) {
|
||||
return false
|
||||
}
|
||||
|
||||
const normalizedCodec = codec.toLowerCase()
|
||||
return (
|
||||
normalizedCodec === MP4_AUDIO_CODEC
|
||||
|| normalizedCodec === 'aac'
|
||||
|| normalizedCodec.startsWith('mp4a.40.2')
|
||||
)
|
||||
}
|
||||
|
||||
private async passthroughAudioStream(
|
||||
audioStream: ReadableStream<EncodedAudioChunk>,
|
||||
audioConfig: AudioDecoderConfig,
|
||||
muxer: VideoMuxer,
|
||||
): Promise<boolean> {
|
||||
if (!this.isPassthroughAudioCodec(audioConfig.codec)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const reader = audioStream.getReader()
|
||||
let wroteAudio = false
|
||||
let passthroughTimestampOffsetUs: number | null = null
|
||||
|
||||
try {
|
||||
while (!this.cancelled) {
|
||||
const { done, value: chunk } = await reader.read()
|
||||
if (done || !chunk) break
|
||||
|
||||
if (passthroughTimestampOffsetUs === null) {
|
||||
passthroughTimestampOffsetUs = Math.min(0, chunk.timestamp)
|
||||
}
|
||||
|
||||
const normalizedTimestamp = Math.max(
|
||||
0,
|
||||
chunk.timestamp - passthroughTimestampOffsetUs,
|
||||
)
|
||||
const outputChunk = passthroughTimestampOffsetUs === 0
|
||||
? chunk
|
||||
: this.cloneEncodedAudioChunkWithTimestamp(chunk, normalizedTimestamp)
|
||||
|
||||
await muxer.addAudioChunk(
|
||||
outputChunk,
|
||||
wroteAudio
|
||||
? undefined
|
||||
: {
|
||||
decoderConfig: audioConfig,
|
||||
},
|
||||
)
|
||||
wroteAudio = true
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
await reader.cancel()
|
||||
} catch {
|
||||
// reader already closed
|
||||
}
|
||||
}
|
||||
|
||||
return wroteAudio
|
||||
}
|
||||
|
||||
/**
|
||||
* Audio export has two modes:
|
||||
* 1) no speed regions -> fast WebCodecs trim-only pipeline
|
||||
@@ -65,9 +129,64 @@ export class AudioProcessor {
|
||||
return
|
||||
}
|
||||
|
||||
if (sortedTrims.length === 0) {
|
||||
let audioConfig: AudioDecoderConfig
|
||||
try {
|
||||
audioConfig = (await demuxer.getDecoderConfig('audio')) as AudioDecoderConfig
|
||||
} catch {
|
||||
console.warn('[AudioProcessor] No audio track found, skipping')
|
||||
return
|
||||
}
|
||||
|
||||
const audioStream = typeof readEndSec === 'number'
|
||||
? demuxer.read('audio', 0, readEndSec)
|
||||
: demuxer.read('audio')
|
||||
|
||||
const copiedSourceAudio = await this.passthroughAudioStream(
|
||||
audioStream as ReadableStream<EncodedAudioChunk>,
|
||||
audioConfig,
|
||||
muxer,
|
||||
)
|
||||
|
||||
if (copiedSourceAudio) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await this.processTrimOnlyAudio(demuxer, muxer, sortedTrims, readEndSec)
|
||||
}
|
||||
|
||||
async renderEditedAudioTrack(
|
||||
videoUrl: string,
|
||||
trimRegions?: TrimRegion[],
|
||||
speedRegions?: SpeedRegion[],
|
||||
audioRegions?: AudioRegion[],
|
||||
sourceAudioFallbackPaths?: string[],
|
||||
): Promise<Blob> {
|
||||
const sortedTrims = trimRegions ? [...trimRegions].sort((a, b) => a.startMs - b.startMs) : []
|
||||
const sortedSpeedRegions = speedRegions
|
||||
? [...speedRegions]
|
||||
.filter((region) => region.endMs - region.startMs > MIN_SPEED_REGION_DELTA_MS)
|
||||
.sort((a, b) => a.startMs - b.startMs)
|
||||
: []
|
||||
const sortedAudioRegions = audioRegions
|
||||
? [...audioRegions].sort((a, b) => a.startMs - b.startMs)
|
||||
: []
|
||||
const sortedSourceAudioFallbackPaths = sourceAudioFallbackPaths
|
||||
? sourceAudioFallbackPaths.filter(
|
||||
(audioPath) => typeof audioPath === 'string' && audioPath.trim().length > 0,
|
||||
)
|
||||
: []
|
||||
|
||||
return this.renderMixedTimelineAudio(
|
||||
videoUrl,
|
||||
sortedTrims,
|
||||
sortedSpeedRegions,
|
||||
sortedAudioRegions,
|
||||
sortedSourceAudioFallbackPaths,
|
||||
)
|
||||
}
|
||||
|
||||
// Legacy trim-only path used when no speed regions are configured.
|
||||
private async processTrimOnlyAudio(
|
||||
demuxer: WebDemuxer,
|
||||
@@ -728,6 +847,21 @@ export class AudioProcessor {
|
||||
})
|
||||
}
|
||||
|
||||
private cloneEncodedAudioChunkWithTimestamp(
|
||||
src: EncodedAudioChunk,
|
||||
newTimestamp: number,
|
||||
): EncodedAudioChunk {
|
||||
const data = new Uint8Array(src.byteLength)
|
||||
src.copyTo(data)
|
||||
|
||||
return new EncodedAudioChunk({
|
||||
type: src.type,
|
||||
timestamp: newTimestamp,
|
||||
duration: src.duration ?? undefined,
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
private isInTrimRegion(timestampMs: number, trims: TrimRegion[]) {
|
||||
return trims.some((trim) => timestampMs >= trim.startMs && timestampMs < trim.endMs)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user