Closed Bug 2035059 Opened 23 days ago Closed 19 days ago

Certain MP3 files report their length as Infinite instead of actual length when playing with HTML5 Audio element

Categories

(Core :: Audio/Video: Playback, defect, P3)

Firefox 147
defect

Tracking

()

RESOLVED FIXED
152 Branch
Tracking Status
firefox152 --- fixed

People

(Reporter: philkindleness, Assigned: kinetik)

Details

Attachments

(4 files)

Attached file HTML5AudioDemo.zip

Steps to reproduce:

  1. Using the attached archive, try to play the file "1.mp3" with Player.html. Open the browser console to see the reported value, or look at the time indicator on the player.
  2. Repeat the process with "2.mp3".
  3. Finally, repeat the process with "3.mp3".

Actual results:

  1. "1.mp3" reports that its length is Infinite.
  2. "2.mp3" correctly reports that it is 1 minute and 29 seconds long.
  3. "3.mp3" reports that its length is also Infinite.

Expected results:

  1. "1.mp3" should report that it is 1 minute long.
  2. "2.mp3" should report that it is 1 minute and 29 seconds long.
  3. "3.mp3" should report that it is 2 minutes and 21 seconds long.
Flags: needinfo?(kinetik)
Severity: -- → S3
Status: UNCONFIRMED → NEW
Component: Untriaged → Audio/Video: Playback
Ever confirmed: true
OS: Unspecified → All
Priority: -- → P3
Product: Firefox → Core
Hardware: Unspecified → All

HTMLMediaElement.duration is reported as Infinity when an MP3 without a Xing/Info VBR header is loaded via a data: URL. The same files load with the correct duration via file: and blob: URLs.

This affects KhinoPlayer because Player.html does FileReader.readAsDataURL(file) and assigns the result to audio.src, so every load goes via a data: URL.

What's different between the three attached files

File Duration First audio frame offset Xing/Info header Notes
1.mp3 60.08 s 56 178 No CBR 192k; ID3v2 contains a 1282×720 cover art JPEG
2.mp3 88.99 s 17 142 Yes (Info @ 17178) CBR 320k; ID3v2 with 300×300 cover art
3.mp3 141.59 s 173 186 No CBR 192k; ID3v2 holds ~240 bytes of real frames followed by ~173 KB of zero padding

Only file 2 carries an Info header (the CBR variant of Xing) immediately after the ID3 tag. Files 1 and 3 are plain CBR streams whose duration must be derived as (streamLength − firstFrameOffset) × 8 / bitrate.

A small test page that loads each file via direct file URL, blob URL, and data URL gives:

1.mp3   direct=60.082    blob=60.082    data=Infinity (intermittent)
2.mp3   direct=88.987    blob=88.987    data=88.987
3.mp3   direct=141.589   blob=141.589   data=Infinity (more reliably)

file: and blob: URLs always produce the correct duration. Only data: produces Infinity, and only for files without a Xing/Info header. The failure is timing-dependent, but file 3 races almost every time.

Assignee: nobody → kinetik
Status: NEW → ASSIGNED
Flags: needinfo?(kinetik)

ChannelMediaResource::OnStartRequest only read Content-Length for HTTP
channels. For data: URIs (and jar:, resource:, ...) the local length
stayed at -1, so MediaCacheStream::NotifyDataStarted was told the
length was unknown, the cache stream length never got set, and demuxers
that estimate duration from the stream length saw -1 and returned
Nothing (which surfaces in JS as audio.duration === Infinity).

This is what made KhinoPlayer-style players that base64-encode mp3
files and assign them to audio.src report Infinite duration for files
without a complete Xing/Info header (e.g. Torchlight Title.mp3).

For the non-HTTP branch, ask the channel for its content length and
fall back to -1 if it doesn't know. Channels that genuinely don't know
their length (e.g. live streams over a custom protocol) are unaffected.

The mochitest fetches a 30-second CBR mp3 (no Xing header), turns it
into a data: URL via FileReader.readAsDataURL the way KhinoPlayer does,
and verifies the audio element reports a finite, correct duration.

If MP3TrackDemuxer::Init runs before the resource length is known (e.g.
a CBR mp3 with no Xing/Info header loaded via a non-HTTP transport that
reports Content-Length late), Duration() returns Nothing and we were
storing TimeUnit::FromInfinity() in mInfo->mDuration. That bubbles up
to MediaFormatReader::OnDemuxerInitDone, which sets
mInfo.mMetadataDuration = Some(Infinity) because Infinity is treated as
"positive". Then MediaDecoderStateMachine::OnMetadataRead unconditionally
assigns mMaster->mDuration = Info().mMetadataDuration, clobbering any
finite duration BufferedRangeUpdated has already produced from cached
byte ranges.

Logging confirms the race in the data:-URL repro: BufferedRangeUpdated
sets mDuration to 30.024 s, then OnMetadataRead overwrites it back to
Infinity, and audio.duration stays Infinity for the lifetime of the
element even though audio.seekable.end is finite.

Only assign mInfo->mDuration when we actually have a finite duration.
Leaving it at the default zero keeps mInfo.mMetadataDuration unset, so
the state machine preserves the duration BufferedRangeUpdated computes,
and the durationchange event delivers a finite value.

The mochitest now waits for canplaythrough rather than loadedmetadata
so it observes the post-metadata-read state where this race resolves.

ADTSTrackDemuxer::Init has the same shape as the MP3 case fixed in the
preceding commit: when StreamLength() is unknown, Duration() returns
TimeUnit::FromInfinity(), and writing that into mInfo->mDuration
propagates Some(Infinity) into mInfo.mMetadataDuration in
MediaFormatReader. OnMetadataRead in the state machine then clobbers
any finite mDuration that BufferedRangeUpdated has already produced.

Only assign mInfo->mDuration when Duration() returns a finite value, so
mInfo.mMetadataDuration stays unset and the buffered-ranges-driven
duration update isn't overwritten.

The mochitest is the analog of test_mp3_dataurl_duration.html for ADTS.
The byte-size-divided-by-average-frame-length estimate the demuxer uses
is ~8% off for variable-rate AAC, so the tolerance is wide; this test's
job is to catch the Infinity-latching regression, not the unrelated
estimation accuracy.

Wave and Flac don't share this shape: WaveDemuxer reads mDataLength
from the data chunk header at init and Duration() returns a finite
value regardless of stream length; FlacTrackDemuxer::Duration returns
max(parsed-frame-duration, info.mDuration), neither of which is ever
Infinity.

Pushed by mgregan@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/e52a8be985e3 https://hg.mozilla.org/integration/autoland/rev/7079b643af3f Pick up non-HTTP channel content length for media duration r=alwu,media-playback-reviewers https://github.com/mozilla-firefox/firefox/commit/68d577da6d4d https://hg.mozilla.org/integration/autoland/rev/d5fce2457d0f Don't latch Infinity in MP3 demuxer mInfo->mDuration r=alwu,media-playback-reviewers https://github.com/mozilla-firefox/firefox/commit/11606654c86b https://hg.mozilla.org/integration/autoland/rev/9d854b97973e Don't latch Infinity in ADTS demuxer mInfo->mDuration r=alwu,media-playback-reviewers
Status: ASSIGNED → RESOLVED
Closed: 19 days ago
Resolution: --- → FIXED
Target Milestone: --- → 152 Branch
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Creator:
Created:
Updated:
Size: