mirror of
https://github.com/arcodange-org/mediabunny.git
synced 2026-09-28 03:13:51 +02:00
Add metadata-based duration retrieval
This commit is contained in:
@@ -143,6 +143,10 @@ export class AdtsDemuxer extends Demuxer {
|
||||
this.lastLoadedPos = header.startPos + header.frameLength;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(): Promise<number | null> {
|
||||
return null; // No way
|
||||
}
|
||||
|
||||
async getMimeType() {
|
||||
return 'audio/aac';
|
||||
}
|
||||
@@ -227,6 +231,10 @@ class AdtsAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getLiveRefreshInterval() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,22 @@ import { Input } from './input';
|
||||
import { InputTrack } from './input-track';
|
||||
import { MetadataTags } from './metadata';
|
||||
|
||||
/**
|
||||
* Options for retrieving media duration from metadata.
|
||||
* @group Input files & tracks
|
||||
* @public
|
||||
*/
|
||||
export type DurationMetadataRequestOptions = {
|
||||
/**
|
||||
* When the underlying media is live, querying the duration will, by default, wait until the live stream has ended.
|
||||
* Setting this field to `true` skips that wait and returns the current duration of the stream. When the media isn't
|
||||
* live, this field has no effect.
|
||||
*
|
||||
* See also {@link PacketRetrievalOptions.skipLiveWait}.
|
||||
*/
|
||||
skipLiveWait?: boolean;
|
||||
};
|
||||
|
||||
export abstract class Demuxer {
|
||||
input: Input;
|
||||
|
||||
@@ -20,6 +36,7 @@ export abstract class Demuxer {
|
||||
abstract getTracks(): Promise<InputTrack[]>;
|
||||
abstract getMimeType(): Promise<string>;
|
||||
abstract getMetadataTags(): Promise<MetadataTags>;
|
||||
abstract getDurationFromMetadata(options: DurationMetadataRequestOptions): Promise<number | null>;
|
||||
|
||||
dispose() {
|
||||
// Can be overridden
|
||||
|
||||
@@ -97,6 +97,17 @@ export class FlacDemuxer extends Demuxer {
|
||||
return [this.track];
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(): Promise<number | null> {
|
||||
await this.readMetadata();
|
||||
assert(this.audioInfo);
|
||||
|
||||
if (this.audioInfo.totalSamples === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.audioInfo.totalSamples / this.audioInfo.sampleRate;
|
||||
}
|
||||
|
||||
async getMimeType() {
|
||||
return 'audio/flac';
|
||||
}
|
||||
@@ -252,6 +263,10 @@ export class FlacDemuxer extends Demuxer {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.audioInfo) {
|
||||
throw new Error('Missing STREAMINFO metadata block! Corrupted FLAC file.');
|
||||
}
|
||||
})());
|
||||
}
|
||||
|
||||
@@ -578,6 +593,10 @@ class FlacAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDurationFromMetadata() {
|
||||
return this.demuxer.getDurationFromMetadata();
|
||||
}
|
||||
|
||||
async getLiveRefreshInterval() {
|
||||
return null;
|
||||
}
|
||||
|
||||
+18
-2
@@ -1,5 +1,5 @@
|
||||
import { AUDIO_CODECS, AudioCodec, inferCodecFromCodecString, MediaCodec, VIDEO_CODECS, VideoCodec } from '../codec';
|
||||
import { Demuxer } from '../demuxer';
|
||||
import { Demuxer, DurationMetadataRequestOptions } from '../demuxer';
|
||||
import { Input } from '../input';
|
||||
import {
|
||||
InputAudioTrack,
|
||||
@@ -52,6 +52,7 @@ export class HlsDemuxer extends Demuxer {
|
||||
metadataPromise: Promise<void> | null = null;
|
||||
tracks: InputTrack[] = [];
|
||||
segmentedInputs: HlsSegmentedInput[] = [];
|
||||
hasMasterPlaylist = true;
|
||||
|
||||
constructor(input: Input) {
|
||||
super(input);
|
||||
@@ -153,7 +154,8 @@ export class HlsDemuxer extends Demuxer {
|
||||
} else if (line.startsWith('#EXTINF:')) {
|
||||
// This is a media playlist, not a master playlist
|
||||
const segmentedInput = new HlsSegmentedInput(this, this.input._entryPath, lines);
|
||||
this.segmentedInputs.push(segmentedInput);
|
||||
this.segmentedInputs = [segmentedInput];
|
||||
this.hasMasterPlaylist = false;
|
||||
|
||||
const input = segmentedInput.toInput();
|
||||
this.tracks = await input.getTracks();
|
||||
@@ -569,6 +571,12 @@ export class HlsDemuxer extends Demuxer {
|
||||
return segmentedInput;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(options: DurationMetadataRequestOptions): Promise<number | null> {
|
||||
return this.hasMasterPlaylist
|
||||
? null
|
||||
: this.segmentedInputs[0]!.getDurationFromMetadata(options);
|
||||
}
|
||||
|
||||
async getMetadataTags(): Promise<MetadataTags> {
|
||||
return {};
|
||||
}
|
||||
@@ -693,6 +701,14 @@ abstract class HlsInputTrackBacking implements InputTrackBacking {
|
||||
return this.internalTrack.averageBitrate;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(options: DurationMetadataRequestOptions): Promise<number | null> {
|
||||
if (!this.internalTrack.backingTrack) {
|
||||
throw new TrackNotHydratedError();
|
||||
}
|
||||
|
||||
return this.internalTrack.backingTrack._backing.getDurationFromMetadata(options);
|
||||
}
|
||||
|
||||
async getLiveRefreshInterval(): Promise<number | null> {
|
||||
if (!this.internalTrack.backingTrack) {
|
||||
throw new TrackNotHydratedError();
|
||||
|
||||
@@ -12,7 +12,6 @@ const IV_STRING_REGEX = /^0[xX][0-9a-fA-F]+$/;
|
||||
export type HlsSegment = Segment & {
|
||||
sequenceNumber: number | null;
|
||||
location: HlsSegmentLocation;
|
||||
duration: number;
|
||||
encryption: HlsEncryptionInfo | null;
|
||||
firstSegment: HlsSegment | null;
|
||||
initSegment: HlsSegment | null;
|
||||
|
||||
+29
-3
@@ -11,10 +11,11 @@ import { determineVideoPacketType } from './codec-data';
|
||||
import { customAudioDecoders, customVideoDecoders } from './custom-coder';
|
||||
import { Input } from './input';
|
||||
import { EncodedPacketSink, PacketRetrievalOptions } from './media-sink';
|
||||
import { assert, MaybePromise, NonFunctionKeys, Rational, Rotation, simplifyRational } from './misc';
|
||||
import { assert, MaybePromise, NonFunctionKeys, Rational, Rotation, roundToDivisor, simplifyRational } from './misc';
|
||||
import { TrackType } from './output';
|
||||
import { EncodedPacket, PacketType } from './packet';
|
||||
import { TrackDisposition } from './metadata';
|
||||
import { DurationMetadataRequestOptions } from './demuxer';
|
||||
|
||||
/**
|
||||
* Contains aggregate statistics about the encoded packets of a track.
|
||||
@@ -44,6 +45,7 @@ export interface InputTrackBacking {
|
||||
getPairingMask(): bigint;
|
||||
getBitrate(): number | null;
|
||||
getAverageBitrate(): number | null;
|
||||
getDurationFromMetadata(options: DurationMetadataRequestOptions): Promise<number | null>;
|
||||
getLiveRefreshInterval(): Promise<number | null>;
|
||||
getHasOnlyKeyPackets?(): boolean | null;
|
||||
|
||||
@@ -209,7 +211,27 @@ export abstract class InputTrack {
|
||||
}
|
||||
|
||||
const lastPacket = await this._backing.getPacket(Infinity, { metadataOnly: true, ...options });
|
||||
return (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
|
||||
const result = (lastPacket?.timestamp ?? 0) + (lastPacket?.duration ?? 0);
|
||||
|
||||
return roundToDivisor(result, this.timeResolution);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the duration (end timestamp) of this track from metadata stored in the file. This value may be
|
||||
* approximate or diverge from the actual, precise duration returned by `.computeDuration()`, but compared to that
|
||||
* method, this method is very cheap. When the duration cannot be determined from the file metadata, `null`
|
||||
* is returned.
|
||||
*
|
||||
* By default, when the underlying media is live, this method will only resolve once the live stream
|
||||
* ends. If you want to query the current duration of the media, set
|
||||
* {@link DurationMetadataRequestOptions.skipLiveWait} to `true` in the options.
|
||||
*/
|
||||
async getDurationFromMetadata(options: DurationMetadataRequestOptions = {}) {
|
||||
if (!this.isHydrated) {
|
||||
await this.hydrate();
|
||||
}
|
||||
|
||||
return this._backing.getDurationFromMetadata(options);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,7 +285,11 @@ export abstract class InputTrack {
|
||||
return (await this._backing.getLiveRefreshInterval()) !== null;
|
||||
}
|
||||
|
||||
getLiveRefreshInterval() {
|
||||
async getLiveRefreshInterval() {
|
||||
if (!this.isHydrated) {
|
||||
await this.hydrate();
|
||||
}
|
||||
|
||||
return this._backing.getLiveRefreshInterval();
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Demuxer } from './demuxer';
|
||||
import { Demuxer, DurationMetadataRequestOptions } from './demuxer';
|
||||
import { InputFormat } from './input-format';
|
||||
import {
|
||||
InputAudioTrack,
|
||||
@@ -321,6 +321,21 @@ export class Input<S extends Source = Source> implements Disposable {
|
||||
return Math.min(...firstTimestamps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the duration (end timestamp) of the input file from metadata stored in the file. This value may be
|
||||
* approximate or diverge from the actual, precise duration returned by `.computeDuration()`, but compared to that
|
||||
* method, this method is very cheap. When the duration cannot be determined from the file metadata, `null`
|
||||
* is returned.
|
||||
*
|
||||
* By default, when the underlying media is live, this method will only resolve once the live stream
|
||||
* ends. If you want to query the current duration of the media, set
|
||||
* {@link DurationMetadataRequestOptions.skipLiveWait} to `true` in the options.
|
||||
*/
|
||||
async getDurationFromMetadata(options: DurationMetadataRequestOptions = {}) {
|
||||
const demuxer = await this._getDemuxer();
|
||||
return demuxer.getDurationFromMetadata(options);
|
||||
}
|
||||
|
||||
/** Returns the list of all tracks of this input file in the order in which they appear in the file. */
|
||||
async getTracks(query?: TrackQuery<InputTrack>) {
|
||||
const demuxer = await this._getDemuxer();
|
||||
|
||||
@@ -220,18 +220,6 @@ const ascii = (text: string, nullTerminated = false) => {
|
||||
return bytes;
|
||||
};
|
||||
|
||||
const lastPresentedSample = (samples: Sample[]) => {
|
||||
let result: Sample | null = null;
|
||||
|
||||
for (const sample of samples) {
|
||||
if (!result || sample.timestamp > result.timestamp) {
|
||||
result = sample;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const rotationMatrix = (rotationInDegrees: number): TransformationMatrix => {
|
||||
const theta = rotationInDegrees * (Math.PI / 180);
|
||||
const cosTheta = Math.round(Math.cos(theta));
|
||||
@@ -353,11 +341,7 @@ export const mvhd = (
|
||||
const duration = intoTimescale(Math.max(
|
||||
0,
|
||||
...trackDatas
|
||||
.filter(x => x.samples.length > 0)
|
||||
.map((x) => {
|
||||
const lastSample = lastPresentedSample(x.samples)!;
|
||||
return lastSample.timestamp + lastSample.duration;
|
||||
}),
|
||||
.map(x => presentationSpan(x)),
|
||||
), GLOBAL_TIMESCALE);
|
||||
const nextTrackId = Math.max(0, ...trackDatas.map(x => x.track.id)) + 1;
|
||||
|
||||
@@ -379,6 +363,30 @@ export const mvhd = (
|
||||
]);
|
||||
};
|
||||
|
||||
const presentationSpan = (trackData: IsobmffTrackData) => {
|
||||
if (trackData.samples.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let minTimestamp = Infinity;
|
||||
let maxEndTimestamp = -Infinity;
|
||||
|
||||
for (const sample of trackData.samples) {
|
||||
if (sample.timestamp < minTimestamp) {
|
||||
minTimestamp = sample.timestamp;
|
||||
}
|
||||
if (sample.timestamp + sample.duration > maxEndTimestamp) {
|
||||
maxEndTimestamp = sample.timestamp + sample.duration;
|
||||
}
|
||||
}
|
||||
|
||||
if (minTimestamp === Infinity) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return maxEndTimestamp - minTimestamp;
|
||||
};
|
||||
|
||||
/**
|
||||
* Track Box: Defines a single track of a movie. A movie may consist of one or more tracks. Each track is
|
||||
* independent of the other tracks in the movie and carries its own temporal and spatial information. Each Track Box
|
||||
@@ -405,9 +413,8 @@ export const tkhd = (
|
||||
trackData: IsobmffTrackData,
|
||||
creationTime: number,
|
||||
) => {
|
||||
const lastSample = lastPresentedSample(trackData.samples);
|
||||
const durationInGlobalTimescale = intoTimescale(
|
||||
lastSample ? lastSample.timestamp + lastSample.duration : 0,
|
||||
presentationSpan(trackData),
|
||||
GLOBAL_TIMESCALE,
|
||||
);
|
||||
|
||||
@@ -456,9 +463,8 @@ export const mdhd = (
|
||||
trackData: IsobmffTrackData,
|
||||
creationTime: number,
|
||||
) => {
|
||||
const lastSample = lastPresentedSample(trackData.samples);
|
||||
const localDuration = intoTimescale(
|
||||
lastSample ? lastSample.timestamp + lastSample.duration : 0,
|
||||
presentationSpan(trackData),
|
||||
trackData.timescale,
|
||||
);
|
||||
|
||||
|
||||
@@ -447,6 +447,26 @@ export class IsobmffDemuxer extends Demuxer {
|
||||
})();
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(): Promise<number | null> {
|
||||
await this.readMetadata();
|
||||
|
||||
if (this.movieDurationInTimescale <= 0) {
|
||||
// The duration is often zero for fragmented files for example; return `null` to signal that the duration
|
||||
// must be computed instead.
|
||||
return null;
|
||||
}
|
||||
|
||||
let endTimestamp = this.movieDurationInTimescale / this.movieTimescale;
|
||||
if (this.tracks.length > 0) {
|
||||
const minFirstTimestamp = Math.min(
|
||||
...(await Promise.all(this.tracks.map(x => x.inputTrack!.getFirstTimestamp()))),
|
||||
);
|
||||
endTimestamp += minFirstTimestamp;
|
||||
}
|
||||
|
||||
return endTimestamp;
|
||||
}
|
||||
|
||||
getSampleTableForTrack(internalTrack: InternalTrack) {
|
||||
if (internalTrack.sampleTable) {
|
||||
return internalTrack.sampleTable;
|
||||
@@ -2560,6 +2580,17 @@ abstract class IsobmffTrackBacking implements InputTrackBacking {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata() {
|
||||
const track = this.internalTrack;
|
||||
if (track.durationInMediaTimescale <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
assert(track.inputTrack);
|
||||
|
||||
return (await track.inputTrack.getFirstTimestamp()) + track.durationInMediaTimescale / track.timescale;
|
||||
}
|
||||
|
||||
async getLiveRefreshInterval() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -316,6 +316,29 @@ export class MatroskaDemuxer extends Demuxer {
|
||||
return metadataTags;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(): Promise<number | null> {
|
||||
await this.readMetadata();
|
||||
|
||||
let maxEndTimestamp: number | null = null;
|
||||
for (const segment of this.segments) {
|
||||
if (segment.duration > 0) {
|
||||
let endTimestamp = segment.duration / segment.timestampFactor;
|
||||
|
||||
if (segment.tracks.length > 0) {
|
||||
const minFirstTimestamp = Math.min(
|
||||
...await Promise.all(segment.tracks.map(x => x.inputTrack!.getFirstTimestamp())),
|
||||
);
|
||||
|
||||
endTimestamp += minFirstTimestamp;
|
||||
}
|
||||
|
||||
maxEndTimestamp = Math.max(maxEndTimestamp ?? -Infinity, endTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
return maxEndTimestamp;
|
||||
}
|
||||
|
||||
readMetadata() {
|
||||
return this.readMetadataPromise ??= (async () => {
|
||||
let currentPos = 0;
|
||||
@@ -1970,6 +1993,15 @@ abstract class MatroskaTrackBacking implements InputTrackBacking {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata() {
|
||||
const segment = this.internalTrack.segment;
|
||||
if (segment.tracks.length > 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.internalTrack.demuxer.getDurationFromMetadata();
|
||||
}
|
||||
|
||||
async getLiveRefreshInterval() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -152,7 +152,8 @@ export class MatroskaMuxer extends Muxer {
|
||||
firstMsTimestamp: number;
|
||||
}>();
|
||||
|
||||
private duration = 0;
|
||||
private startTimestamp = Infinity;
|
||||
private endTimestamp = -Infinity;
|
||||
|
||||
constructor(output: Output, format: MkvOutputFormat) {
|
||||
super(output);
|
||||
@@ -1197,7 +1198,8 @@ export class MatroskaMuxer extends Muxer {
|
||||
this.ebmlWriter.writeEBML(blockGroup);
|
||||
}
|
||||
|
||||
this.duration = Math.max(this.duration, msTimestamp + msDuration);
|
||||
this.startTimestamp = Math.min(this.startTimestamp, msTimestamp);
|
||||
this.endTimestamp = Math.max(this.endTimestamp, msTimestamp + msDuration);
|
||||
trackData.lastWrittenMsTimestamp = msTimestamp;
|
||||
|
||||
if (!this.trackDatasInCurrentCluster.has(trackData)) {
|
||||
@@ -1325,7 +1327,10 @@ export class MatroskaMuxer extends Muxer {
|
||||
this.ebmlWriter.writeVarInt(segmentSize, SEGMENT_SIZE_BYTES);
|
||||
|
||||
// Write the duration of the media to the Segment
|
||||
this.segmentDuration!.data = new EBMLFloat64(this.duration);
|
||||
const duration = this.startTimestamp === Infinity
|
||||
? 0
|
||||
: this.endTimestamp - this.startTimestamp;
|
||||
this.segmentDuration!.data = new EBMLFloat64(duration);
|
||||
this.writer.seek(this.ebmlWriter.offsets.get(this.segmentDuration!)!);
|
||||
this.ebmlWriter.writeEBML(this.segmentDuration);
|
||||
|
||||
|
||||
+78
-2
@@ -12,9 +12,23 @@ import { Input } from '../input';
|
||||
import { InputAudioTrack, InputAudioTrackBacking } from '../input-track';
|
||||
import { DEFAULT_TRACK_DISPOSITION, MetadataTags } from '../metadata';
|
||||
import { PacketRetrievalOptions } from '../media-sink';
|
||||
import { assert, AsyncMutex, binarySearchExact, binarySearchLessOrEqual, UNDETERMINED_LANGUAGE } from '../misc';
|
||||
import {
|
||||
assert,
|
||||
AsyncMutex,
|
||||
binarySearchExact,
|
||||
binarySearchLessOrEqual,
|
||||
toDataView,
|
||||
UNDETERMINED_LANGUAGE,
|
||||
} from '../misc';
|
||||
import { EncodedPacket, PLACEHOLDER_DATA } from '../packet';
|
||||
import { Mp3FrameHeader, getXingOffset, INFO, XING } from '../../shared/mp3-misc';
|
||||
import {
|
||||
Mp3FrameHeader,
|
||||
getXingOffset,
|
||||
INFO,
|
||||
XING,
|
||||
XingFlags,
|
||||
computeAverageMp3FrameSize,
|
||||
} from '../../shared/mp3-misc';
|
||||
import {
|
||||
ID3_V1_TAG_SIZE,
|
||||
ID3_V2_HEADER_SIZE,
|
||||
@@ -37,8 +51,13 @@ export class Mp3Demuxer extends Demuxer {
|
||||
|
||||
metadataPromise: Promise<void> | null = null;
|
||||
firstFrameHeader: Mp3FrameHeader | null = null;
|
||||
firstFrameHeaderPos: number | null = null;
|
||||
loadedSamples: Sample[] = []; // All samples from the start of the file to lastLoadedPos
|
||||
metadataTags: MetadataTags | null = null;
|
||||
xingData: {
|
||||
frameCount: number | null;
|
||||
fileSize: number | null;
|
||||
} | null = null;
|
||||
|
||||
tracks: InputAudioTrack[] = [];
|
||||
|
||||
@@ -109,12 +128,33 @@ export class Mp3Demuxer extends Demuxer {
|
||||
|
||||
if (isXing) {
|
||||
// There's no actual audio data in this frame, so let's skip it
|
||||
|
||||
if (!this.xingData) {
|
||||
let xingDataSlice = this.reader.requestSlice(result.startPos + xingOffset + 4, 12);
|
||||
if (xingDataSlice instanceof Promise) xingDataSlice = await xingDataSlice;
|
||||
if (xingDataSlice) {
|
||||
const xingData = readBytes(xingDataSlice, 12);
|
||||
const view = toDataView(xingData);
|
||||
const flags = view.getUint32(0, false);
|
||||
|
||||
this.xingData = {
|
||||
frameCount: (flags & XingFlags.FrameCount)
|
||||
? view.getUint32(4, false)
|
||||
: null,
|
||||
fileSize: (flags & XingFlags.FileSize)
|
||||
? view.getUint32(8, false)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.firstFrameHeader) {
|
||||
this.firstFrameHeader = header;
|
||||
this.firstFrameHeaderPos = result.startPos;
|
||||
}
|
||||
|
||||
if (header.sampleRate !== this.firstFrameHeader.sampleRate) {
|
||||
@@ -138,6 +178,38 @@ export class Mp3Demuxer extends Demuxer {
|
||||
return;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(): Promise<number | null> {
|
||||
await this.readMetadata();
|
||||
assert(this.firstFrameHeader !== null);
|
||||
assert(this.firstFrameHeaderPos !== null);
|
||||
|
||||
if (this.xingData) {
|
||||
if (this.xingData.frameCount !== null) {
|
||||
return this.xingData.frameCount
|
||||
* this.firstFrameHeader.audioSamplesInFrame
|
||||
/ this.firstFrameHeader.sampleRate;
|
||||
}
|
||||
} else {
|
||||
// No Xing, assuming CBR
|
||||
|
||||
if (this.reader.fileSize !== null) {
|
||||
const averageFrameSize = computeAverageMp3FrameSize(
|
||||
this.firstFrameHeader.lowSamplingFrequency,
|
||||
this.firstFrameHeader.layer,
|
||||
this.firstFrameHeader.bitrate,
|
||||
this.firstFrameHeader.sampleRate,
|
||||
);
|
||||
const frameCount = (this.reader.fileSize - this.firstFrameHeaderPos) / averageFrameSize;
|
||||
|
||||
return Math.round(frameCount)
|
||||
* this.firstFrameHeader.audioSamplesInFrame
|
||||
/ this.firstFrameHeader.sampleRate;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async getMimeType() {
|
||||
return 'audio/mpeg';
|
||||
}
|
||||
@@ -237,6 +309,10 @@ class Mp3AudioTrackBacking implements InputAudioTrackBacking {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDurationFromMetadata() {
|
||||
return this.demuxer.getDurationFromMetadata();
|
||||
}
|
||||
|
||||
async getLiveRefreshInterval() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { assert, toDataView } from '../misc';
|
||||
import { toDataView } from '../misc';
|
||||
import { metadataTagsAreEmpty } from '../metadata';
|
||||
import { Muxer } from '../muxer';
|
||||
import { Output, OutputAudioTrack } from '../output';
|
||||
@@ -107,14 +107,14 @@ export class Mp3Muxer extends Muxer {
|
||||
|
||||
this.validateAndNormalizeTimestamp(track, packet.timestamp, packet.type === 'key');
|
||||
|
||||
if (writeXingHeader) {
|
||||
this.framePositions.push(this.writer.getPos());
|
||||
}
|
||||
|
||||
this.writer.write(packet.data);
|
||||
this.frameCount++;
|
||||
|
||||
await this.writer.flush();
|
||||
|
||||
if (writeXingHeader) {
|
||||
this.framePositions.push(this.writer.getPos());
|
||||
}
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
@@ -132,20 +132,20 @@ export class Mp3Muxer extends Muxer {
|
||||
const release = await this.mutex.acquire();
|
||||
|
||||
const endPos = this.writer.getPos();
|
||||
const audioDataEndPos = endPos - this.xingFramePos;
|
||||
|
||||
this.writer.seek(this.xingFramePos);
|
||||
|
||||
const toc = new Uint8Array(100);
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const index = Math.floor(this.framePositions.length * (i / 100));
|
||||
assert(index !== -1 && index < this.framePositions.length);
|
||||
|
||||
const byteOffset = this.framePositions[index]!;
|
||||
toc[i] = 256 * (byteOffset / endPos);
|
||||
const byteOffset = this.framePositions[index]! - this.xingFramePos;
|
||||
toc[i] = 256 * (byteOffset / audioDataEndPos);
|
||||
}
|
||||
|
||||
this.xingFrameData.frameCount = this.frameCount;
|
||||
this.xingFrameData.fileSize = endPos;
|
||||
this.xingFrameData.fileSize = audioDataEndPos;
|
||||
this.xingFrameData.toc = toc;
|
||||
|
||||
if (this.format._options.onXingFrame) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
getXingOffset,
|
||||
KILOBIT_RATES,
|
||||
XING,
|
||||
XingFlags,
|
||||
} from '../../shared/mp3-misc';
|
||||
|
||||
export type XingFrameData = {
|
||||
@@ -96,13 +97,13 @@ export class Mp3Writer {
|
||||
|
||||
let flags = 0;
|
||||
if (data.frameCount !== null) {
|
||||
flags |= 1;
|
||||
flags |= XingFlags.FrameCount;
|
||||
}
|
||||
if (data.fileSize !== null) {
|
||||
flags |= 2;
|
||||
flags |= XingFlags.FileSize;
|
||||
}
|
||||
if (data.toc !== null) {
|
||||
flags |= 4;
|
||||
flags |= XingFlags.Toc;
|
||||
}
|
||||
|
||||
this.writeU32(flags);
|
||||
|
||||
@@ -708,6 +708,10 @@ export class MpegTsDemuxer extends Demuxer {
|
||||
return this.tracks;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(): Promise<number | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getMetadataTags(): Promise<MetadataTags> {
|
||||
return {}; // Nothing for now
|
||||
}
|
||||
@@ -1098,6 +1102,10 @@ abstract class MpegTsTrackBacking implements InputTrackBacking {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getLiveRefreshInterval() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -386,6 +386,10 @@ export class OggDemuxer extends Demuxer {
|
||||
});
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(): Promise<number | null> {
|
||||
return null; // Not stored anywhere
|
||||
}
|
||||
|
||||
async getTracks() {
|
||||
await this.readMetadata();
|
||||
return this.tracks;
|
||||
@@ -464,6 +468,10 @@ class OggAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata() {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getLiveRefreshInterval() {
|
||||
return null;
|
||||
}
|
||||
|
||||
+21
-1
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import { AudioCodec, MediaCodec, VideoCodec } from './codec';
|
||||
import { Demuxer } from './demuxer';
|
||||
import { Demuxer, DurationMetadataRequestOptions } from './demuxer';
|
||||
import { Input } from './input';
|
||||
import { VirtualInputFormat } from './input-format';
|
||||
import {
|
||||
@@ -42,6 +42,7 @@ export type AssociatedGroup = {
|
||||
|
||||
export type Segment = {
|
||||
timestamp: number;
|
||||
duration: number;
|
||||
relativeToUnixEpoch: boolean;
|
||||
firstSegment: Segment | null;
|
||||
};
|
||||
@@ -75,6 +76,17 @@ export abstract class SegmentedInput {
|
||||
|
||||
abstract getLiveRefreshInterval(): Promise<number | null>;
|
||||
|
||||
async getDurationFromMetadata(options: DurationMetadataRequestOptions) {
|
||||
const lastSegment = await this.getSegmentAt(Infinity, {
|
||||
skipLiveWait: options.skipLiveWait,
|
||||
});
|
||||
if (!lastSegment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return lastSegment.timestamp + lastSegment.duration;
|
||||
}
|
||||
|
||||
toInput() {
|
||||
return this.virtualInput ??= new Input({
|
||||
source: new NullSource(),
|
||||
@@ -102,6 +114,10 @@ class SegmentedInputDemuxer extends Demuxer {
|
||||
this.segmentedInput = segmentedInput;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(): Promise<number | null> {
|
||||
throw new Error('Unreachable');
|
||||
}
|
||||
|
||||
async getMetadataTags(): Promise<MetadataTags> {
|
||||
throw new Error('Unreachable');
|
||||
}
|
||||
@@ -253,6 +269,10 @@ class SegmentedInputInputTrackBacking implements InputTrackBacking {
|
||||
return this.firstInputTrack._backing.getAverageBitrate();
|
||||
}
|
||||
|
||||
getDurationFromMetadata(options: DurationMetadataRequestOptions): Promise<number | null> {
|
||||
return this.demuxer.segmentedInput.getDurationFromMetadata(options);
|
||||
}
|
||||
|
||||
getLiveRefreshInterval(): Promise<number | null> {
|
||||
return this.demuxer.segmentedInput.getLiveRefreshInterval();
|
||||
}
|
||||
|
||||
@@ -328,6 +328,13 @@ export class WaveDemuxer extends Demuxer {
|
||||
return null;
|
||||
}
|
||||
|
||||
async getDurationFromMetadata(): Promise<number | null> {
|
||||
await this.readMetadata();
|
||||
assert(this.dataSize !== -1);
|
||||
|
||||
return this.dataSize / this.audioInfo!.blockSizeInBytes / this.audioInfo!.sampleRate;
|
||||
}
|
||||
|
||||
async getMimeType() {
|
||||
return 'audio/wav';
|
||||
}
|
||||
@@ -414,6 +421,10 @@ class WaveAudioTrackBacking implements InputAudioTrackBacking {
|
||||
return null;
|
||||
}
|
||||
|
||||
getDurationFromMetadata() {
|
||||
return this.demuxer.getDurationFromMetadata();
|
||||
}
|
||||
|
||||
async getLiveRefreshInterval() {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user