diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2b2155..e06b340 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,7 +19,14 @@ jobs: with: fetch-depth: 0 + - name: Enforce stable releases target main + if: ${{ !github.event.release.prerelease && github.event.release.target_commitish != 'main' }} + uses: actions/github-script@ffc2c79a5b2490bd33e0a41c1de74b877714d736 # v3.2.0 + with: + script: "core.setFailed('Non-prerelease releases must target the main branch (got: ${{ github.event.release.target_commitish }}).')" + - name: Merge main into release branch + if: ${{ !github.event.release.prerelease }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" @@ -35,7 +42,7 @@ jobs: registry-url: https://registry.npmjs.org - name: Update npm - run: npm install -g npm@latest + run: npm install -g npm@11.11.0 - name: Get package.json version id: package-json-version @@ -59,10 +66,38 @@ jobs: - name: Upload build artifacts env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh release upload ${{ github.event.release.tag_name }} dist/bundles/mediabunny.cjs dist/bundles/mediabunny.min.cjs dist/bundles/mediabunny.mjs dist/bundles/mediabunny.min.mjs dist/mediabunny.d.ts packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.js packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.mjs packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.mjs packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts - - - name: Publish Mediabunny to npm - run: npm publish --access public + run: > + gh release upload ${{ github.event.release.tag_name }} + dist/bundles/mediabunny.cjs + dist/bundles/mediabunny.min.cjs + dist/bundles/mediabunny.mjs + dist/bundles/mediabunny.min.mjs + dist/mediabunny.d.ts + packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.js + packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.js + packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.mjs + packages/mp3-encoder/dist/bundles/mediabunny-mp3-encoder.min.mjs + packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts + packages/ac3/dist/bundles/mediabunny-ac3.js + packages/ac3/dist/bundles/mediabunny-ac3.min.js + packages/ac3/dist/bundles/mediabunny-ac3.mjs + packages/ac3/dist/bundles/mediabunny-ac3.min.mjs + packages/ac3/dist/mediabunny-ac3.d.ts + packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.js + packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.min.js + packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.mjs + packages/aac-encoder/dist/bundles/mediabunny-aac-encoder.min.mjs + packages/aac-encoder/dist/mediabunny-aac-encoder.d.ts + packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.js + packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.min.js + packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.mjs + packages/flac-encoder/dist/bundles/mediabunny-flac-encoder.min.mjs + packages/flac-encoder/dist/mediabunny-flac-encoder.d.ts + packages/server/dist/bundles/mediabunny-server.cjs + packages/server/dist/bundles/mediabunny-server.min.cjs + packages/server/dist/bundles/mediabunny-server.mjs + packages/server/dist/bundles/mediabunny-server.min.mjs + packages/server/dist/mediabunny-server.d.ts - name: Publish workspace packages to npm - run: npm publish --access public --workspaces + run: npm publish --access public --workspaces ${{ github.event.release.prerelease && '--tag beta' || '' }} diff --git a/.gitignore b/.gitignore index a359208..10904ca 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,8 @@ node_modules /docs/api *.tsbuildinfo -packages/mp3-encoder/dist \ No newline at end of file +packages/mp3-encoder/dist +packages/ac3/dist +packages/aac-encoder/dist +packages/flac-encoder/dist +packages/server/dist \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 3b881ed..a8e60ea 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,6 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }, - "typescript.tsdk": "node_modules/typescript/lib" + "typescript.tsdk": "node_modules/typescript/lib", + "js/ts.tsdk.path": "node_modules/typescript/lib" } diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..83413a7 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +- Prefer functions declared using const, not using the function keyword +- Code style is tab indent with semicolons +- Mediabunny core code is contained in src/, extensions are in packages/*/, website is in docs/ +- Tests: Prefer fewer, longer test files over many small ones. Test files should be named after the general catergory of thing that is being tested, not after any individual single test. +- Avoid ifs without a {} block. So no if (cond) return;, always do if (cond) { return; } +- `type` instead of `interface` for object types \ No newline at end of file diff --git a/README.md b/README.md index 6d108d4..40bc2c3 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ -Mediabunny is a JavaScript library for reading, writing, and converting media files (like MP4, WebM, MP3), directly in the browser. It aims to be a complete toolkit for high-performance media operations on the web. It's written from scratch in pure TypeScript, has zero dependencies, is very performant, and is extremely tree-shakable, meaning you only include what you use. You can think of it a bit like [FFmpeg](https://ffmpeg.org/), but built from the ground up for the web. +Mediabunny is a JavaScript library for reading, writing, and converting media (like MP4, WebM, MP3, HLS), directly in the browser. It aims to be a complete toolkit for high-performance media operations on the web. It's written from scratch in pure TypeScript, has zero dependencies, is very performant, and is extremely tree-shakable, meaning you only include what you use. You can think of it a bit like [FFmpeg](https://ffmpeg.org/), but built from the ground up for the web. [Documentation](https://mediabunny.dev) | [Examples](https://mediabunny.dev/examples) | [Sponsoring](#sponsoring) | [License](#license) | [Discord](https://discord.gg/hmpkyYuS4U) @@ -17,7 +17,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi
- Remotion + + + Remotion +      @@ -31,15 +34,19 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi Kino +      + + Screen Studio + +      + + Tella +
### Silver sponsors
- - Pintura Labs - -      Ponder @@ -48,6 +55,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi ### Bronze sponsors
+ + ElevenLabs + +      React Video Editor @@ -67,14 +78,14 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi Core features include: -- **Wide format support**: Read and write MP4, MOV, WebM, MKV, WAVE, MP3, Ogg, ADTS, FLAC -- **Built-in encoding & decoding**: Supports 25+ video, audio, and subtitle codecs, hardware-accelerated using the WebCodecs API -- **High precision**: Fine-grained, microsecond-accurate reading and writing operations -- **Conversion API**: Easy-to-use API with features such as transmuxing, transcoding, resizing, rotation, cropping, resampling, trimming, and more -- **Streaming I/O**: Handle reading & writing files of any size with memory-efficient streaming -- **Tree-shakable**: Only bundle what you use (as small as 5 kB gzipped) -- **Zero dependencies**: Implemented in highly performant TypeScript -- **Cross-platform**: Works in browsers and Node.js +- Wide format support: Read _and_ write MP4, MOV, WebM, MKV, HLS, WAVE, MP3, Ogg, ADTS, FLAC, MPEG-TS +- Built-in encoding & decoding: Supports 25+ video, audio, and subtitle codecs, hardware-accelerated using the WebCodecs API +- High precision: Fine-grained, microsecond-accurate reading and writing operations +- Conversion API: Easy-to-use API with features such as transmuxing, transcoding, resizing, rotation, cropping, resampling, trimming, and more +- Streaming I/O: Handle reading & writing files of any size with memory-efficient streaming +- Cross-platform: Works in all browsers as well as in Node, Bun, and Deno using [`@mediabunny/server`](https://github.com/Vanilagy/mediabunny/blob/main/packages/server/README.md) +- Tree-shakable: Only bundle what you use (as small as 5 kB gzipped) +- Zero dependencies: Implemented in highly performant TypeScript [See full feature list](https://mediabunny.dev/guide/introduction#features) @@ -100,16 +111,23 @@ Requires any JavaScript environment that can run ECMAScript 2021 or later. Media ```js import { Input, ALL_FORMATS, BlobSource } from 'mediabunny'; +// Reading from disk const input = new Input({ - source: new BlobSource(file), // Reading from disk + source: new BlobSource(file), formats: ALL_FORMATS, }); const duration = await input.computeDuration(); // in seconds const videoTrack = await input.getPrimaryVideoTrack(); const audioTrack = await input.getPrimaryAudioTrack(); -const { displayWidth, displayHeight, rotation } = videoTrack; -const { sampleRate, numberOfChannels } = audioTrack; + +const displayWidth = await videoTrack.getDisplayWidth(); +const displayHeight = await videoTrack.getDisplayHeight(); +const rotation = await videoTrack.getRotation(); + +const sampleRate = await audioTrack.getSampleRate(); +const numberOfChannels = await audioTrack.getNumberOfChannels(); + const { title, artist, album } = await input.getMetadataTags(); ``` diff --git a/build.sh b/build.sh deleted file mode 100755 index e1d5e58..0000000 --- a/build.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -e - -# This script must be executed via `npm run build` - -# Clear the stuff from last build -rm -rf dist -rm -rf packages/mp3-encoder/dist - -# Ensure license headers on all source files -tsx scripts/ensure-license-headers.ts - -# Type check & generate .js and .d.ts files -tsc -p src -tsc -p packages/mp3-encoder/src - -# So that the resulting files use valid ESM imports with file extension. This only runs for the core Mediabunny as only -# it ships the individual files to npm (for tree shaking, because it's large) -npm run fix-build-import-paths - -# Creates bundles for all packages -tsx scripts/bundle.ts - -# Declaration file rollup and checks -api-extractor run -api-extractor run -c packages/mp3-encoder/api-extractor.json - -# Checks that all symbols are documented -tsx scripts/check-docblocks.ts dist/mediabunny.d.ts -tsx scripts/check-docblocks.ts packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts - -# Checks that API docs are generatable -npm run docs:generate -- --dry - -# Appends stuff to the declaration files to register the global variables these libraries expose -echo 'export as namespace Mediabunny;' >> dist/mediabunny.d.ts -echo 'export as namespace MediabunnyMp3Encoder;' >> packages/mp3-encoder/dist/mediabunny-mp3-encoder.d.ts \ No newline at end of file diff --git a/dev/convert.html b/dev/convert.html index e548520..c94dbb9 100644 --- a/dev/convert.html +++ b/dev/convert.html @@ -2,9 +2,13 @@ + + \ No newline at end of file diff --git a/dev/demux.html b/dev/demux.html index cab4bd7..ac9fba2 100644 --- a/dev/demux.html +++ b/dev/demux.html @@ -1,8 +1,11 @@ + \ No newline at end of file + diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index a1e44e7..7b779a6 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,13 +1,19 @@ +/* eslint-disable @stylistic/max-len */ import { withMermaid } from 'vitepress-plugin-mermaid'; import footnote from 'markdown-it-footnote'; import tailwindcss from '@tailwindcss/vite'; import llmstxt from 'vitepress-plugin-llms'; import { HeadConfig } from 'vitepress'; -// @ts-expect-error This file gets generated once docs:generate is run +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore This file gets generated once docs:generate is run import apiRoutes from '../api/index.json'; +import m3u8Grammar from './m3u8-grammar.json' with { type: 'json' }; +import fs from 'node:fs/promises'; +import path from 'node:path'; const DESCRIPTION = 'A JavaScript library for reading, writing, and converting media files. Directly in the browser,' + ' and faster than anybunny else.'; +const ORIGIN = 'https://mediabunny.dev'; // https://vitepress.dev/reference/site-config export default withMermaid({ @@ -15,21 +21,31 @@ export default withMermaid({ description: DESCRIPTION, cleanUrls: true, sitemap: { - hostname: 'https://mediabunny.dev', + hostname: ORIGIN, + transformItems: async (items) => { + const entries = await fs.readdir('./examples'); + for (const entry of entries) { + const isDirectory = await fs.stat(path.join('./examples', entry)).then(stat => stat.isDirectory()); + if (isDirectory) { + items.push({ + url: `/examples/${entry}/`, // With trailing slash + }); + } + } + + return items; + }, }, + // lastUpdated: true, head: [ ['link', { rel: 'icon', type: 'image/png', href: '/mediabunny-logo.png' }], ['link', { rel: 'icon', type: 'image/svg+xml', href: '/mediabunny-logo.svg' }], - ['meta', { property: 'og:type', content: 'website' }], ['meta', { property: 'og:site_name', content: 'Mediabunny' }], - ['meta', { property: 'og:url', content: 'https://mediabunny.dev/' }], - ['meta', { property: 'og:image', content: 'https://mediabunny.dev/mediabunny-og-image.png' }], + ['meta', { property: 'og:image', content: `${ORIGIN}/mediabunny-og-image.png` }], ['meta', { property: 'og:locale', content: 'en-US' }], - ['meta', { property: 'og:description', content: DESCRIPTION }], - ['meta', { name: 'twitter:image', content: 'https://mediabunny.dev/mediabunny-og-image.png' }], + ['meta', { name: 'twitter:image', content: `${ORIGIN}/mediabunny-og-image.png` }], ['meta', { name: 'twitter:card', content: 'summary_large_image' }], ['meta', { name: 'twitter:site', content: '@vanilagy' }], - ['meta', { name: 'twitter:description', content: DESCRIPTION }], ], themeConfig: { logo: '/mediabunny-logo.svg', @@ -37,11 +53,18 @@ export default withMermaid({ // https://vitepress.dev/reference/default-theme-config nav: [ { text: 'Guide', link: '/guide/introduction', activeMatch: '/guide' }, - { text: 'API', link: '/api/', activeMatch: '/api' }, + { text: 'API', link: '/api/', activeMatch: '/api/' }, // Trailing slash because it's index.html from there { text: 'LLMs', link: '/llms', activeMatch: '/llms' }, { text: 'Examples', link: '/examples', activeMatch: '/examples' }, + { text: 'Blog', link: '/blog', activeMatch: '/blog' }, { text: 'Sponsors', link: '/#sponsors', activeMatch: '/#sponsors' }, - { text: 'License', link: 'https://github.com/Vanilagy/mediabunny#license' }, + { text: 'License', link: 'https://github.com/Vanilagy/mediabunny#license', rel: 'noopener' }, + { + text: 'More', + items: [ + { text: 'Codec Registry', link: '/codec-registry/overview' }, + ], + }, ], sidebar: { @@ -83,15 +106,61 @@ export default withMermaid({ { text: 'Supported formats & codecs', link: '/guide/supported-formats-and-codecs' }, ], }, + { + text: 'HLS', + items: [ + { text: 'Reading HLS', link: '/guide/reading-hls' }, + { text: 'Writing HLS', link: '/guide/writing-hls' }, + ], + }, { text: 'Extensions', items: [ + { text: 'server', link: '/guide/extensions/server' }, { text: 'mp3-encoder', link: '/guide/extensions/mp3-encoder' }, + { text: 'aac-encoder', link: '/guide/extensions/aac-encoder' }, + { text: 'ac3', link: '/guide/extensions/ac3' }, + { text: 'flac-encoder', link: '/guide/extensions/flac-encoder' }, + ], + }, + ], + + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment + '/api': apiRoutes as any, + + '/codec-registry': [ + { + text: 'Codec registry', + items: [ + { text: 'Overview', link: '/codec-registry/overview' }, + ], + }, + { + text: 'Video', + items: [ + { text: 'AVC (H.264)', link: '/codec-registry/avc' }, + { text: 'HEVC (H.265)', link: '/codec-registry/hevc' }, + { text: 'VP8', link: '/codec-registry/vp8' }, + { text: 'VP9', link: '/codec-registry/vp9' }, + { text: 'AV1', link: '/codec-registry/av1' }, + ], + }, + { + text: 'Audio', + items: [ + { text: 'AAC', link: '/codec-registry/aac' }, + { text: 'Opus', link: '/codec-registry/opus' }, + { text: 'MP3', link: '/codec-registry/mp3' }, + { text: 'Vorbis', link: '/codec-registry/vorbis' }, + { text: 'FLAC', link: '/codec-registry/flac' }, + { text: 'AC-3', link: '/codec-registry/ac3' }, + { text: 'E-AC-3', link: '/codec-registry/eac3' }, + { text: 'Linear PCM', link: '/codec-registry/pcm' }, + { text: 'μ-law PCM', link: '/codec-registry/ulaw' }, + { text: 'A-law PCM', link: '/codec-registry/alaw' }, ], }, ], - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - '/api': apiRoutes, }, socialLinks: [ @@ -111,12 +180,15 @@ export default withMermaid({ footer: { message: 'Released under the Mozilla Public License 2.0.', - copyright: 'Copyright © 2025-present Vanilagy', + copyright: `Copyright © ${new Date().getFullYear()}-present Vanilagy`, }, }, markdown: { math: true, theme: { light: 'github-light', dark: 'github-dark-dimmed' }, + shikiSetup: (shiki) => { + shiki.loadLanguageSync(m3u8Grammar); + }, config(md) { md.use(footnote); }, @@ -128,6 +200,8 @@ export default withMermaid({ llmstxt({ ignoreFiles: [ 'api/*', + 'examples.md', + 'llms.md', ], }), ], @@ -139,9 +213,95 @@ export default withMermaid({ title += ' | Mediabunny'; } + const canonicalUrl = `${ORIGIN}/${pageData.relativePath}` + .replace(/index\.md$/, '') + .replace(/\.md$/, ''); + const isBlogPost = canonicalUrl.includes('/blog/'); + + const breadcrumbs: object[] = []; + + if (canonicalUrl.includes('/guide/')) { + breadcrumbs.push({ + '@type': 'ListItem', + 'position': 1, + 'name': 'Guide', + 'item': `${ORIGIN}/guide`, + }, { + '@type': 'ListItem', + 'position': 2, + 'name': pageData.title, + }); + } + + if (canonicalUrl.includes('/api/')) { + breadcrumbs.push({ + '@type': 'ListItem', + 'position': 1, + 'name': 'API docs', + 'item': `${ORIGIN}/api/`, + }, { + '@type': 'ListItem', + 'position': 2, + 'name': pageData.title, + }); + } + + if (canonicalUrl.includes('/codec-registry/')) { + breadcrumbs.push({ + '@type': 'ListItem', + 'position': 1, + 'name': 'Codec registry', + 'item': `${ORIGIN}/codec-registry/overview`, + }, { + '@type': 'ListItem', + 'position': 2, + 'name': pageData.title, + }); + } + ((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push( + ['meta', { property: 'og:type', content: isBlogPost ? 'article' : 'website' }], ['meta', { property: 'og:title', content: title }], - ['meta', { property: 'twitter:title', content: title }], + ['meta', { property: 'og:description', content: pageData.description || DESCRIPTION }], + ['meta', { property: 'og:url', content: canonicalUrl }], + ['meta', { name: 'twitter:title', content: title }], + ['meta', { name: 'twitter:description', content: pageData.description || DESCRIPTION }], + ['link', { rel: 'canonical', href: canonicalUrl }], ); + + if (isBlogPost) { + ((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push( + ['meta', { property: 'article:published_time', content: String(pageData.frontmatter['publishedOnIso']) }], + ['meta', { property: 'article:author', content: String(pageData.frontmatter['author']) }], + ); + + breadcrumbs.push({ + '@type': 'ListItem', + 'position': 1, + 'name': 'Blog posts', + 'item': `${ORIGIN}/blog`, + }, { + '@type': 'ListItem', + 'position': 2, + 'name': pageData.title, + }); + } + + if (breadcrumbs.length > 0) { + ((pageData.frontmatter['head'] ??= []) as HeadConfig[]).push( + ['script', { type: 'application/ld+json' }, JSON.stringify({ + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + 'itemListElement': breadcrumbs, + })], + ); + } + }, + buildEnd: async () => { + const files = await fs.readdir('./docs/api'); + + for (const file of files) { + await fs.copyFile('./docs/api/' + file, './dist-docs/api/' + file); + } }, }); diff --git a/docs/.vitepress/m3u8-grammar.json b/docs/.vitepress/m3u8-grammar.json new file mode 100644 index 0000000..6a672f0 --- /dev/null +++ b/docs/.vitepress/m3u8-grammar.json @@ -0,0 +1,113 @@ +{ + "name": "m3u8", + "scopeName": "source.m3u8", + "fileTypes": ["m3u8"], + "patterns": [ + { + "match": "^#EXTM3U$", + "name": "keyword.control.m3u8" + }, + { + "begin": "^(#)(EXTINF)(:)", + "beginCaptures": { + "1": { "name": "punctuation.definition.comment.m3u8" }, + "2": { "name": "keyword.control.extinf.m3u8" }, + "3": { "name": "punctuation.separator.key-value.m3u8" } + }, + "end": "$", + "patterns": [ + { + "match": "\\b\\d+(?:\\.\\d+)?\\b", + "name": "string.unquoted.m3u8" + }, + { + "match": ",", + "name": "punctuation.separator.sequence.m3u8" + }, + { + "match": "[^,\\n]+", + "name": "string.unquoted.title.m3u8" + } + ] + }, + { + "begin": "^(#)(EXT-X-[A-Z0-9-]+|EXT-[A-Z0-9-]+)(:)", + "beginCaptures": { + "1": { "name": "punctuation.definition.comment.m3u8" }, + "2": { "name": "keyword.control.tag.m3u8" }, + "3": { "name": "punctuation.separator.key-value.m3u8" } + }, + "end": "$", + "patterns": [ + { + "include": "#attribute-list" + }, + { + "match": "\\b(?:YES|NO)\\b", + "name": "string.unquoted.m3u8" + }, + { + "match": "\\b\\d+(?:\\.\\d+)?\\b", + "name": "string.unquoted.m3u8" + }, + { + "match": "[^,\\n]+", + "name": "string.unquoted.m3u8" + } + ] + }, + { + "match": "^(#)(EXT-X-[A-Z0-9-]+|EXT-[A-Z0-9-]+)$", + "captures": { + "1": { "name": "punctuation.definition.comment.m3u8" }, + "2": { "name": "keyword.control.tag.m3u8" } + } + }, + { + "match": "^[^#\\s][^\\n]*$", + "name": "source.m3u8.uri" + }, + { + "match": "#.*$", + "name": "comment.line.number-sign.m3u8" + } + ], + "repository": { + "attribute-list": { + "patterns": [ + { + "match": "\\b[A-Z0-9-]+(?==)", + "name": "variable.parameter.m3u8" + }, + { + "match": "=", + "name": "keyword.operator.assignment.m3u8" + }, + { + "match": ",", + "name": "punctuation.separator.sequence.m3u8" + }, + { + "match": "\"[^\"]*\"", + "name": "string.quoted.double.m3u8" + }, + { + "match": "\\b\\d+x\\d+\\b", + "name": "string.unquoted.m3u8" + }, + { + "match": "\\b\\d+(?:\\.\\d+)?\\b", + "name": "string.unquoted.m3u8" + }, + { + "match": "\\b(?:YES|NO)\\b", + "name": "string.unquoted.m3u8" + }, + { + "match": "\\b[A-Z0-9_-]+\\b", + "name": "string.unquoted.m3u8" + } + ] + } + } +} \ No newline at end of file diff --git a/docs/api-config.json b/docs/api-config.json index 1cd9433..453cd8e 100644 --- a/docs/api-config.json +++ b/docs/api-config.json @@ -1,6 +1,7 @@ { "heading": "Mediabunny API reference", "intro": "Here you can find detailed documentation for all classes, functions, constants and types exposed by Mediabunny's public API.", + "description": "Detailed documentation for all classes, functions, constants and types exposed by Mediabunny's public API.", "Samples": "Raw, unencoded chunks of media data, such as video frames or sections of audio.", "Packets": "Chunks of encoded media data.", @@ -16,8 +17,13 @@ "Metadata tags": "Descriptive metadata tags attached to media files.", "Codecs": "Codecs understood by Mediabunny.", "Encoding": "Encoder configuration and encodability checks.", + "Decoding": "Decoder configuration and decodability checks.", "Custom coders": "API for adding custom encoders and decoders.", "Miscellaneous": "Whatever's left.", - "@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny." + "@mediabunny/server": "Adds full video/audio decoder and encoder support to Mediabunny running in server-side environments such as Node, Bun, or Deno.", + "@mediabunny/mp3-encoder": "Adds MP3 encoder support to Mediabunny.", + "@mediabunny/ac3": "Adds AC-3/E-AC-3 decoder and encoder support to Mediabunny.", + "@mediabunny/aac-encoder": "Polyfills AAC encoder support to Mediabunny.", + "@mediabunny/flac-encoder": "Adds FLAC encoder support to Mediabunny." } diff --git a/docs/assets/codec-soup.svg b/docs/assets/codec-soup.svg index 32b746b..22969f2 100644 --- a/docs/assets/codec-soup.svg +++ b/docs/assets/codec-soup.svg @@ -1,46 +1,56 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/assets/german-live-tv.png b/docs/assets/german-live-tv.png new file mode 100644 index 0000000..77be209 Binary files /dev/null and b/docs/assets/german-live-tv.png differ diff --git a/docs/blog.data.ts b/docs/blog.data.ts new file mode 100644 index 0000000..980fd82 --- /dev/null +++ b/docs/blog.data.ts @@ -0,0 +1,3 @@ +import { createContentLoader } from 'vitepress'; + +export default createContentLoader('blog/*.md'); diff --git a/docs/blog.md b/docs/blog.md new file mode 100644 index 0000000..1ff4f3b --- /dev/null +++ b/docs/blog.md @@ -0,0 +1,29 @@ +--- +title: Blog posts +description: Announcements, news, and devblogs about Mediabunny. +outline: false +--- + + + +

Blog posts

+ + \ No newline at end of file diff --git a/docs/blog/mediabunny-now-supports-hls.md b/docs/blog/mediabunny-now-supports-hls.md new file mode 100644 index 0000000..24d379e --- /dev/null +++ b/docs/blog/mediabunny-now-supports-hls.md @@ -0,0 +1,262 @@ +--- +title: Mediabunny now supports HLS! +description: Mediabunny v1.42.0 adds read/write support for HTTP Live Streaming (HLS), pushing the frontier of fully client-side media processing. +publishedOn: April 24, 2026 +publishedOnIso: "2026-04-24" +author: Vanilagy +authorImage: /vani.png +authorLink: https://github.com/Vanilagy +authorSubtitle: Creator of Mediabunny +headerImage: /on-air.png +excerpt: Mediabunny v1.42.0 adds read/write support for HTTP Live Streaming (HLS). This has been in the making for the last ~4 months and is, by far, the biggest addition to Mediabunny since its original release. +--- + + + + + +

{{ $frontmatter.publishedOn }}

+ +

{{ $frontmatter.title }}

+ + + +Mediabunny v1.42.0 adds support for HTTP Live Streaming (HLS). This has been in the making for the last ~4 months and is, by far, the biggest addition to Mediabunny since its original release. Like the rest of Mediabunny, it has been implemented from scratch with zero dependencies and is tiny (adds about ~30 kB of additional bundle size). + +If you wanna jump right into detailed guides for reading and writing HLS, check out [Reading HLS](../guide/reading-hls) and [Writing HLS](../guide/writing-hls). For the full release notes, see [v1.42.0](https://github.com/Vanilagy/mediabunny/releases/tag/v1.42.0). + +## What is HTTP Live Streaming (HLS)? + +For those unfamiliar, HLS is a protocol used to stream media over the internet using HTTP. At its core, it chunks a long piece of media (like a TV episode) into many short segments, each of which is individually addressable via HTTP, and then provides an index called a "playlist" to the user telling them about the available segments. The client then requests whichever segments it needs. + +HLS also enables multiple variants and renditions of the same contents. Typically, this means the same video/audio content encoded with multiple bitrates, resolutions and codecs, and multiple audio tracks, one for each language. + +## Supported features + +One of the reasons that HLS has been in the works for so long is because I've been extremely thorough with the supported feature set. + +Mediabunny supports: +- HLS reading **and** writing +- both VOD and live content +- both unencrypted and encrypted content (including DRM-protected content) +- an arbitrary number of video and audio tracks +- any configuration of variant streams and media renditions +- all segment formats (MPEG-TS, CMAF, fMP4, ADTS, MP3, WAV, ...) +- all codecs (H.264, HEVC, AV1, AAC, MP3, WAVE, AC-3, E-AC-3, ...) +- full lazy loading of track metadata and media, minimizing HTTP requests +- logarithmic seeking performance +- single-file segments via #EXT-X-BYTERANGE and HTTP range requests +- track metadata information (language, name, primary track, autoselect, ...) +- full master playlist configuration for writing +- datetime-stamped media data via #EXT-X-PROGRAM-DATE-TIME +- mid-stream discontinuities +- I-frame only video tracks via #EXT-X-I-FRAME-STREAM-INF +- more, probably + +Most of the above features are implemented _symmetrically_, meaning they are available for both reading and writing operations. + +--- + +To be fully transparent, these features are not yet supported: +- Subtitles (WebVTT, CEA-608/708, ...) +- ID3v2 metadata extraction +- Writing encrypted segments +- Low-latency HLS +- Built-in analytics, ABR, or CMCD (this is for the user to do) + +## Difference to existing solutions like hls.js + +The way Mediabunny enables interaction with HLS playlists is fundamentally different from how existing tools such as [hls.js](https://github.com/video-dev/hls.js) (give them a star!) do it. To put it simply, hls.js is to Mediabunny what a `