Skip to Content
Media Processing

Media Processing

Zaileys ships a standalone Media class for converting and transforming audio, video, images, stickers, and documents before you send them. It wraps ffmpeg plus an image backend (sharp if available, otherwise jimp) behind a small, namespaced facade, and every method returns a plain Buffer (or base64 string / metadata object) that feeds straight into the send builder.

Constructing a Media

Import Media from zaileys and pass it your source once — every namespace below operates on that same input.

import { Media } from 'zaileys' const media = new Media(input)

The constructor accepts a single MediaInput, which is one of:

Input typeExampleNotes
string URL'https://example.com/song.mp3'Fetched over HTTP(S) with the global fetch.
string path'./assets/clip.mp4'Read from disk if the path points to an existing file.
string b64'iVBORw0KGgo...'Falls back to base64 decoding when it is neither URL nor file.
Bufferawait fs.readFile('./img.png')Used as-is.
ArrayBufferawait res.arrayBuffer()Wrapped into a Buffer.
import { Media } from 'zaileys' import fs from 'node:fs/promises' const fromUrl = new Media('https://example.com/voice.mp3') const fromPath = new Media('./assets/photo.jpg') const fromBuffer = new Media(await fs.readFile('./assets/clip.mp4'))

Resolution is lazy. Nothing is downloaded or read until you call a processing method, so constructing a Media is cheap. The same instance can be reused across namespaces — for example call both media.thumbnail.get() and media.image.toJpeg() on one Media.

Requirements: ffmpeg, sharp, and jimp

⚠️

ffmpeg / ffprobe power all audio, video, and animated-sticker work. The published package bundles ffmpeg and ffprobe binaries (via @ffmpeg-installer/ffmpeg and @ffprobe-installer/ffprobe), so most platforms need no system install — they are added to PATH automatically on first use. On platforms without a prebuilt binary (e.g. Termux), install a system ffmpeg (pkg install ffmpeg). See Installation for details.

Image backendimage.toJpeg, image.thumbnail, image.resize, and sticker.create (for non-animated stickers) need an image processor. sharp is used when installed (fast, native). If sharp is absent, Zaileys transparently falls back to the pure-JS jimp path and prints a one-line warning suggesting npm install sharp. Everything works either way — sharp is purely an accelerator.

bash npm i sharp

media.audio

Convert any audio (or the audio track of a video) into WhatsApp-friendly formats. Each method first validates that the source is audio/*.

MethodReturnsDescription
toOpus()Promise<Buffer>Opus in an Ogg container — the format WhatsApp voice notes use. Mono, 48 kHz, 48 kbps.
toMp3()Promise<Buffer>MP3 (libmp3lame). Stereo, 128 kbps.
convert(type?)Promise<Buffer>Convert to 'opus' (default) or 'mp3'. toOpus/toMp3 are thin wrappers around this.
waveform()Promise<{ waveform: Uint8Array; seconds: number }>A 64-sample amplitude envelope (values 0–100) plus duration in seconds, for voice-note waveform display.
import { Client, Media } from 'zaileys' const client = new Client({ /* ... */ }) const media = new Media('./assets/recording.m4a') const opus = await media.audio.toOpus() await client.send('6281234567890@s.whatsapp.net').audio(opus, { ptt: true }) const mp3 = await media.audio.toMp3() // or, equivalently: await media.audio.convert('mp3')
const { waveform, seconds } = await new Media('./assets/voice.ogg').audio.waveform() console.log(seconds) // e.g. 7 console.log(waveform.length) // 64

media.video

Re-encode video for reliable WhatsApp playback or grab a poster frame. Both methods validate that the source is video/*.

MethodReturnsDescription
toMp4()Promise<Buffer>H.264/AAC MP4 with +faststart, yuv420p, even dimensions, crf 28, ultrafast preset.
thumbnail()Promise<string>A 100×100 JPEG poster frame, taken ~10% into the clip, returned base64-encoded.
import { Client, Media } from 'zaileys' const client = new Client({ /* ... */ }) const mp4 = await new Media('https://example.com/clip.mov').video.toMp4() await client.send('6281234567890@s.whatsapp.net').video(mp4, { caption: 'Re-encoded' })
const poster = await new Media('./assets/movie.mp4').video.thumbnail() // `poster` is a base64 JPEG string (no data: prefix)

media.image

MethodReturnsDescription
toJpeg()Promise<Buffer>Convert PNG/WebP to JPEG. Other formats are returned unchanged.
thumbnail()Promise<string>A 100×100 (cover-fit) JPEG, base64-encoded, quality 50. Animated GIFs use the first frame.
resize(width, height)Promise<Buffer>Cover-fit resize to the given pixel dimensions. Output is PNG.
import { Client, Media } from 'zaileys' const client = new Client({ /* ... */ }) const media = new Media('./assets/banner.png') const jpeg = await media.image.toJpeg() await client.send('6281234567890@s.whatsapp.net').image(jpeg, { caption: 'Compressed' }) const small = await media.image.resize(640, 640) const thumb = await media.image.thumbnail() // base64 JPEG string

media.sticker

Turn an image, GIF, or video into a WhatsApp WebP sticker. Animated sources (GIF/video) produce animated stickers (capped at 6 s, 10 fps, 512×512); static images become static stickers. Already-WebP inputs are passed through. EXIF pack metadata is embedded automatically.

create(metadata?: StickerMetadataType): Promise<Buffer>

StickerMetadataType options (all optional):

OptionTypeDefaultDescription
packageNamestring'Zaileys Library'Sticker pack name stored in EXIF.
authorNamestring'https://github.com/zeative/zaileys'Sticker pack publisher stored in EXIF.
qualitynumber60WebP quality, clamped to 1–100.
shape'circle' | 'rounded' | 'oval' | 'default''default'Crops static stickers to a shape mask. (Static only.)
import { Client, Media } from 'zaileys' const client = new Client({ /* ... */ }) const sticker = await new Media('./assets/cat.png').sticker.create({ packageName: 'My Pack', authorName: 'Me', quality: 80, shape: 'rounded', }) await client.send('6281234567890@s.whatsapp.net').sticker(sticker)
// Animated sticker from a GIF or short video const animated = await new Media('./assets/loop.gif').sticker.create({ quality: 70 }) await client.send('6281234567890@s.whatsapp.net').sticker(animated)

shape only affects static stickers. Animated stickers are always padded to a square. Both static and animated paths require their image backend / ffmpeg — see the requirements callout above.

media.document

Wrap any file as a sendable document, auto-detecting its MIME type, extension, and a thumbnail (video poster frame or image thumbnail, empty for non-media files).

create(): Promise<{ document: Buffer mimetype: string ext: string fileName: string jpegThumbnail: string }>
FieldTypeDescription
documentBufferThe raw file bytes.
mimetypestringDetected MIME type (e.g. application/pdf).
extstringDetected file extension (e.g. pdf).
fileNamestringAn auto-generated unique id (override it when sending).
jpegThumbnailstringBase64 JPEG thumbnail for media files; empty string otherwise.
import { Client, Media } from 'zaileys' const client = new Client({ /* ... */ }) const doc = await new Media('./assets/report.pdf').document.create() await client.send('6281234567890@s.whatsapp.net').document(doc.document, { fileName: 'report.pdf', mimetype: doc.mimetype, })

media.thumbnail

A convenience that detects the source type and produces a 100×100 base64 JPEG thumbnail — routing to the video poster-frame path for video/* and the image path for image/*.

get(): Promise<string>
import { Media } from 'zaileys' // Works for both images and videos const thumb = await new Media('./assets/whatever.mp4').thumbnail.get()
🚫

thumbnail.get() throws Invalid media type: expected image or video when the source is not an image or video (e.g. a PDF), or when the file type cannot be detected. Wrap it in a try/catch if the input is untrusted.

media.toBuffer()

Resolve the original source to a raw Buffer without any processing — handy for downloading a URL or reading a file once and reusing the bytes.

import { Media } from 'zaileys' const buffer = await new Media('https://example.com/file.bin').toBuffer()

Feeding results into the send builder

Every processing method returns a Buffer (or a metadata object whose document field is a Buffer), and the send builder accepts a Buffer anywhere a MediaSource is expected. So the pattern is always: process → pass the buffer to the matching content method.

import { Client, Media } from 'zaileys' const client = new Client({ /* ... */ }) const jid = '6281234567890@s.whatsapp.net' // Image → sticker const sticker = await new Media('./assets/photo.jpg').sticker.create() await client.send(jid).sticker(sticker) // Any audio → voice note const opus = await new Media('./assets/song.mp3').audio.toOpus() await client.send(jid).audio(opus, { ptt: true }) // Re-encode then send video const mp4 = await new Media('./assets/clip.mov').video.toMp4() await client.send(jid).video(mp4)

You only need Media when you want to transform bytes (convert formats, build stickers, generate thumbnails). To send a file as-is, hand the URL, path, or Buffer directly to the send builder — it handles plain media uploads on its own.

Tips and gotchas

  • Type validation throws. audio.* requires audio/*, video.* requires video/*. Passing a mismatched file rejects with an Invalid file type error.
  • Thumbnails are base64 strings, not buffers. video.thumbnail, image.thumbnail, and thumbnail.get all return base64-encoded JPEG text (no data: prefix).
  • Sticker quality is clamped to 1–100; out-of-range values are coerced.
  • Reuse the instance. Building one Media and calling several namespaces avoids re-reading the source repeatedly when you process the same file in multiple ways.

Incoming media: ctx.media

The Media class above is for outgoing transforms. On the incoming side, every message context exposes an optional ctx.media — a discriminated union on .type that carries the rich payload behind a message. For attachments (image, video, audio, document, sticker) it holds the file metadata plus lazy buffer() / stream() accessors; for richer message kinds it holds the structured fields below. Narrow on ctx.media?.type to read the right shape.

Beyond the attachment and the earlier structured types (poll, contact, location, event), these variants are also surfaced:

type: 'album'

The album header message. The actual photos and videos arrive afterwards as separate image / video messages — this header only announces how many to expect.

FieldTypeDescription
expectedImageCountnumber | nullHow many image messages the album will contain.
expectedVideoCountnumber | nullHow many video messages the album will contain.

type: 'group-invite'

A shared group-invite card.

FieldTypeDescription
groupIdstring | nullJID of the invited group.
groupNamestring | nullDisplay name of the group.
inviteCodestring | nullThe invite code (used to join).
captionstring | nullOptional caption attached to the invite.
expiresAtnumber | nullExpiry timestamp of the invite.

type: 'product'

A product card from a WhatsApp Business catalog.

FieldTypeDescription
productIdstring | nullCatalog product id.
titlestring | nullProduct title.
descriptionstring | nullProduct description.
pricenumber | nullPrice amount.
currencystring | nullISO currency code.
retailerIdstring | nullRetailer-defined SKU / id.
urlstring | nullProduct URL.
businessOwnerIdstring | nullJID of the business that owns the catalog.

type: 'order'

An order summary referencing one or more catalog products.

FieldTypeDescription
orderIdstring | nullOrder id.
titlestring | nullOrder title.
itemCountnumber | nullNumber of items in the order.
totalnumber | nullOrder total amount.
currencystring | nullISO currency code.
status'inquiry' | 'accepted' | 'declined' | nullOrder status.
messagestring | nullAttached message / note.

type: 'payment'

A payment request, send, or invite.

FieldTypeDescription
kind'request' | 'send' | 'invite'Whether it requests, sends, or invites a payment.
amountnumber | nullPayment amount.
currencystring | nullISO currency code.
notestring | nullAttached note.
expiresAtnumber | nullExpiry timestamp.

A plain text message that carries a rich link preview. The chatType stays 'text', but ctx.media holds the unfurled preview.

FieldTypeDescription
urlstring | nullThe previewed URL.
titlestring | nullPreview title.
descriptionstring | nullPreview description.
import { Client } from 'zaileys' const client = new Client({ /* ... */ }) client.on('messages', async (ctx) => { switch (ctx.media?.type) { case 'album': console.log(`Album incoming: ${ctx.media.expectedImageCount} images, ${ctx.media.expectedVideoCount} videos`) break case 'group-invite': console.log(`Invite to ${ctx.media.groupName} (code ${ctx.media.inviteCode})`) break case 'product': console.log(`${ctx.media.title} — ${ctx.media.price} ${ctx.media.currency}`) break case 'order': console.log(`Order ${ctx.media.orderId}: ${ctx.media.itemCount} items, ${ctx.media.status}`) break case 'payment': console.log(`Payment ${ctx.media.kind}: ${ctx.media.amount} ${ctx.media.currency}`) break case 'link': console.log(`Link preview: ${ctx.media.title} → ${ctx.media.url}`) break } })

ctx.media is optional and type-discriminated — always narrow on ctx.media?.type before reading fields, and remember most fields are … | null because WhatsApp omits them freely.

See also: Sending Messages and Installation.

Last updated on