usememos/memos · error

webm has no Opus audio track

Error message

webm has no Opus audio track

What it means

Returned by WebMOpusToWAV (internal/ai/audio) when the parsed WebM container's Segment.Tracks.TrackEntry contains no track with an Opus codec. The converter only supports the Opus audio tracks that browser MediaRecorder produces; a WebM with another codec (or video-only) cannot be converted to WAV for STT.

Source

Thrown at internal/ai/audio/webm.go:51

//
// The output is mono or stereo at 48 kHz (Opus's native decode rate),
// regardless of the original encoder's hint. Pre-skip samples declared in
// the OpusHead are discarded to avoid the encoder's startup padding.
//
// The function reads the entire WebM document into memory; callers should
// enforce their own size limits before invoking it.
func WebMOpusToWAV(input []byte) ([]byte, error) {
	var doc struct {
		Header  webm.EBMLHeader `ebml:"EBML"`
		Segment webm.Segment    `ebml:"Segment"`
	}
	if err := ebml.Unmarshal(bytes.NewReader(input), &doc); err != nil && !errors.Is(err, io.EOF) {
		return nil, errors.Wrap(err, "parse webm")
	}

	track := findOpusTrack(doc.Segment.Tracks.TrackEntry)
	if track == nil {
		return nil, errors.New("webm has no Opus audio track")
	}
	if len(track.CodecPrivate) < opusHeadMinLength {
		return nil, errors.Errorf("invalid OpusHead: expected at least %d bytes, got %d", opusHeadMinLength, len(track.CodecPrivate))
	}

	channels := int(track.Audio.Channels)
	if channels < 1 || channels > 2 {
		return nil, errors.Errorf("unsupported Opus channel count: %d", channels)
	}
	preSkip := int(binary.LittleEndian.Uint16(track.CodecPrivate[10:12]))

	decoder := opus.NewDecoder()
	if err := decoder.Init(opusOutputSampleRate, channels); err != nil {
		return nil, errors.Wrap(err, "init opus decoder")
	}

	pcm := make([]int16, 0, 1<<16)
	frame := make([]int16, maxOpusPacketSamples*channels)

View on GitHub (pinned to 14d757ce1f)

Solutions

  1. Record with an explicitly Opus mime type: MediaRecorder.isTypeSupported('audio/webm;codecs=opus') and use it.
  2. If the input is an uploaded file, validate/convert it server-side (ffmpeg -i in.webm -vn -c:a libopus out.webm) before this path.
  3. For video webm, strip to the audio track first; this converter never reads video tracks.
  4. Reject or transcode unsupported inputs upstream with a clear user-facing message.

Example fix

// before
const rec = new MediaRecorder(stream); // browser-default codec, may not be Opus

// after
const mime = ['audio/webm;codecs=opus', 'audio/webm'].find(m => MediaRecorder.isTypeSupported(m));
const rec = new MediaRecorder(stream, mime ? { mimeType: mime } : undefined);
Defensive patterns

Strategy: validation

Validate before calling

// TypeScript — negotiate an Opus mime before recording
const mime = ["audio/webm;codecs=opus", "audio/webm"].find(m => MediaRecorder.isTypeSupported(m));
if (!mime) throw new Error("This browser cannot record Opus WebM audio");
const rec = new MediaRecorder(stream, { mimeType: mime });

Try / catch

// Go
wav, err := audio.WebMOpusToWAV(blob)
if err != nil {
  if strings.Contains(err.Error(), "no Opus audio track") {
    return status.Errorf(codes.InvalidArgument, "Audio must be WebM with an Opus track; re-record or convert the file")
  }
  return err
}

Prevention

When it happens

Trigger: Feeding WebMOpusToWAV audio recorded with a MediaRecorder mimeType whose audio codec is not Opus (e.g., 'audio/webm;codecs=vorbis' where supported, or a video/webm recording with no audio track); uploading an arbitrary .webm file to the voice/STT path; a malformed file whose tracks parse but expose no Opus entry.

Common situations: Cross-browser differences in MediaRecorder codec support; users uploading non-recorded .webm files to the memo voice input; screen-capture webm (video-only, or VP8/VP9 video + no Opus audio); truncated recordings where the Tracks element lacks the audio entry.

Related errors


AI-assisted analysis of usememos/memos@14d757ce1f (2026-08-15). Data as JSON: /api/errors/2719427bd17aa472. Report an issue: GitHub.