diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b2b2155..a316d30 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,36 @@ 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 + 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 - name: Publish Mediabunny to npm - run: npm publish --access public + run: npm publish --access public ${{ github.event.release.prerelease && '--tag beta' || '' }} - 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..3c8896f 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,7 @@ 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 \ No newline at end of file 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 738f4fb..eb4ca58 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) @@ -34,15 +34,19 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi Kino +      + + Screen Studio + +      + + Tella + ### Silver sponsors
- - Pintura Labs - -      Ponder @@ -51,6 +55,10 @@ Mediabunny is a JavaScript library for reading, writing, and converting media fi ### Bronze sponsors
+ + ElevenLabs + +      React Video Editor @@ -70,7 +78,7 @@ 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 +- **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 @@ -103,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/dev/convert.html b/dev/convert.html index 36a2645..f85cc61 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..65aea09 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..2ed0c09 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,60 @@ 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: '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 +179,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 +199,8 @@ export default withMermaid({ llmstxt({ ignoreFiles: [ 'api/*', + 'examples.md', + 'llms.md', ], }), ], @@ -139,9 +212,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..07644e2 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,12 @@ "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/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/assets/on-air.png b/docs/assets/on-air.png new file mode 100644 index 0000000..f5fc4b8 Binary files /dev/null and b/docs/assets/on-air.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..c79f259 --- /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..8701ff2 --- /dev/null +++ b/docs/blog/mediabunny-now-supports-hls.md @@ -0,0 +1,261 @@ +--- +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 +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 `