├── examples ├── angular │ ├── .gitignore │ ├── src │ │ ├── app │ │ │ ├── app.config.ts │ │ │ ├── app.component.html │ │ │ └── app.component.ts │ │ ├── main.ts │ │ └── index.html │ ├── .vscode │ │ └── settings.json │ ├── tsconfig.app.json │ ├── README.md │ ├── eslint.config.js │ ├── tsconfig.json │ ├── package.json │ └── angular.json ├── vite-react │ ├── src │ │ ├── vite-env.d.ts │ │ ├── main.tsx │ │ └── App.tsx │ ├── .gitignore │ ├── index.html │ ├── README.md │ ├── tsconfig.json │ ├── eslint.config.js │ ├── package.json │ └── vite.config.ts ├── webpack-react │ ├── .gitignore │ ├── src │ │ ├── index.html │ │ ├── main.tsx │ │ └── App.tsx │ ├── .vscode │ │ └── settings.json │ ├── tsconfig.json │ ├── eslint.config.js │ ├── README.md │ ├── package.json │ └── webpack.config.js └── browser-umd │ ├── example.js │ └── index.html ├── .husky └── commit-msg ├── __tests__ ├── fixtures │ └── many_tracks.mp4 ├── jest.d.ts ├── unicode.test.ts ├── locateFile.test.ts ├── concurrentAnalysis.test.ts ├── many_tracks.test.ts ├── args.test.ts ├── error.readChunk.test.ts ├── instantiation.test.ts ├── AudioVideoInterleave.avi.test.ts ├── error.wasmLoading.test.ts ├── jest │ ├── toBeNear.ts │ └── setup.ts ├── freeMXF-mxf1.mxf.test.ts ├── utils.ts ├── coverData.test.ts ├── options.test.ts └── dwsample_mp4_360p.mp4.test.ts ├── .prettierrc.json ├── gulpfile.ts ├── .versionrc.json ├── .gitignore ├── gulp ├── typings │ └── gulp.d.ts ├── index.ts ├── default.ts ├── compile │ ├── zenlib.ts │ ├── mediainfolib.ts │ ├── optimizeWasm.ts │ └── wasm.ts ├── declaration.ts ├── download.ts ├── generate-types │ ├── data │ │ ├── parseCsv.ts │ │ ├── parseXsd.ts │ │ └── getFields.ts │ ├── factories.ts │ └── generate.ts ├── transpile │ ├── babel.ts │ └── rollup.ts ├── constants.ts └── utils.ts ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── FEATURE_REQUEST.md │ └── BUG_REPORT.md ├── CONTRIBUTING.md └── workflows │ └── close_inactive_issues.yml ├── .vscode └── settings.json ├── jest.config.json ├── src ├── error.ts ├── index.ts ├── typeGuard.ts ├── MediaInfoModule.d.ts ├── MediaInfoModule.cpp ├── cli.ts ├── mediaInfoFactory.ts └── MediaInfo.ts ├── patches └── gulp-sourcemaps@3.0.0.patch ├── tsconfig.json ├── README.md ├── LICENSE.txt ├── babel.config.cjs ├── eslint.config.js ├── package.json └── CHANGELOG.md /examples/angular/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /node_modules 3 | /.angular 4 | -------------------------------------------------------------------------------- /examples/vite-react/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/webpack-react/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | .webpack/ 4 | dist/ 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | pnpm commitlint --edit ${1} 5 | -------------------------------------------------------------------------------- /__tests__/fixtures/many_tracks.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buzz/mediainfo.js/main/__tests__/fixtures/many_tracks.mp4 -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "endOfLine": "lf", 4 | "printWidth": 100, 5 | "semi": false, 6 | "singleQuote": true, 7 | "trailingComma": "es5" 8 | } 9 | -------------------------------------------------------------------------------- /gulpfile.ts: -------------------------------------------------------------------------------- 1 | export { 2 | babel, 3 | declaration, 4 | default, 5 | download, 6 | generateTypes, 7 | mediainfolib, 8 | rollup, 9 | wasm, 10 | zenlib, 11 | } from './gulp/index.ts' 12 | -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "header": "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n:::note\nChanges preceding version 0.2.0 are not included in the changelog.\n:::\n" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.svn 2 | *~ 3 | .DS_Store 4 | ThumbsDB 5 | \#*# 6 | .#* 7 | /node_modules 8 | npm-debug.log 9 | /build 10 | /dist 11 | /__tests__/fixtures 12 | !/__tests__/fixtures/many_tracks.mp4 13 | /TODO.txt 14 | -------------------------------------------------------------------------------- /gulp/typings/gulp.d.ts: -------------------------------------------------------------------------------- 1 | // @types/gulp-babel is incomplete as it lacks `envName` property in `options` 2 | 3 | declare module 'gulp-babel' { 4 | function babel(options?: { envName: string }): NodeJS.ReadWriteStream 5 | export default babel 6 | } 7 | -------------------------------------------------------------------------------- /examples/angular/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core' 2 | 3 | export const appConfig: ApplicationConfig = { 4 | providers: [provideZoneChangeDetection({ eventCoalescing: true })], 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /examples/angular/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "eslint.experimental.useFlatConfig": true, 4 | "search.exclude": { 5 | "**/dist": true, 6 | "**/node_modules": true 7 | }, 8 | "editor.rulers": [100] 9 | } 10 | -------------------------------------------------------------------------------- /examples/webpack-react/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mediainfo.js Webpack/React Example 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/webpack-react/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "eslint.experimental.useFlatConfig": true, 4 | "search.exclude": { 5 | "**/dist": true, 6 | "**/node_modules": true 7 | }, 8 | "editor.rulers": [100] 9 | } 10 | -------------------------------------------------------------------------------- /examples/webpack-react/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import App from './App' 4 | 5 | const appEl = document.getElementById('app') 6 | 7 | if (appEl) { 8 | createRoot(appEl).render() 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 🙋 Ask a question on Stack Overflow 4 | url: https://stackoverflow.com/search?q=mediainfo.js 5 | about: Check if your question has been answered at Stack Overflow. Or create a new question. 6 | -------------------------------------------------------------------------------- /examples/angular/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |

MediaInfo Angular Example

2 | 3 | 9 |
10 |

11 | 


--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "editor.formatOnSave": true,
 3 |   "eslint.useFlatConfig": true,
 4 |   "search.exclude": {
 5 |     "**/.husky": true,
 6 |     "**/build": true,
 7 |     "**/dist": true,
 8 |     "**/docs": true,
 9 |     "**/node_modules": true
10 |   },
11 |   "editor.rulers": [100]
12 | }
13 | 


--------------------------------------------------------------------------------
/examples/angular/src/main.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser'
2 | import { appConfig } from './app/app.config'
3 | import { AppComponent } from './app/app.component'
4 | 
5 | bootstrapApplication(AppComponent, appConfig).catch((err: unknown) => {
6 |   console.error(err)
7 | })
8 | 


--------------------------------------------------------------------------------
/examples/vite-react/src/main.tsx:
--------------------------------------------------------------------------------
 1 | import React from 'react'
 2 | import ReactDOM from 'react-dom/client'
 3 | import App from './App.tsx'
 4 | 
 5 | const rootEl = document.getElementById('root')
 6 | 
 7 | if (rootEl) {
 8 |   ReactDOM.createRoot(rootEl).render(
 9 |     
10 |       
11 |     
12 |   )
13 | }
14 | 


--------------------------------------------------------------------------------
/examples/angular/tsconfig.app.json:
--------------------------------------------------------------------------------
 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
 2 | {
 3 |   "extends": "./tsconfig.json",
 4 |   "compilerOptions": {
 5 |     "outDir": "./out-tsc/app",
 6 |     "types": []
 7 |   },
 8 |   "files": [
 9 |     "src/main.ts"
10 |   ],
11 |   "include": [
12 |     "src/**/*.d.ts"
13 |   ]
14 | }
15 | 


--------------------------------------------------------------------------------
/examples/angular/README.md:
--------------------------------------------------------------------------------
 1 | # mediainfo.js Angular example
 2 | 
 3 | ## WASM module
 4 | 
 5 | The `MediaInfoModule.wasm` file is copied during build using `assets` configuration.
 6 | 
 7 | ```json
 8 | "assets": [
 9 |   {
10 |     "input": "node_modules/mediainfo.js/dist",
11 |     "glob": "MediaInfoModule.wasm",
12 |     "output": ""
13 |   }
14 | ],
15 | ```
16 | 


--------------------------------------------------------------------------------
/jest.config.json:
--------------------------------------------------------------------------------
1 | {
2 |   "globalSetup": "/__tests__/jest/setup.ts",
3 |   "setupFilesAfterEnv": ["/__tests__/jest/toBeNear.ts"],
4 |   "testEnvironment": "jest-environment-node",
5 |   "testMatch": ["**/__tests__/**/*.test.ts"],
6 |   "testPathIgnorePatterns": ["/build/", "/dist/", "/node_modules/"],
7 |   "verbose": true
8 | }
9 | 


--------------------------------------------------------------------------------
/__tests__/jest.d.ts:
--------------------------------------------------------------------------------
 1 | /// 
 2 | 
 3 | declare namespace jest {
 4 |   interface Matchers {
 5 |     /**
 6 |      * Use `.toBeNear` when checking if a number is a given offset away from a given value.
 7 |      *
 8 |      * @param {Number} value
 9 |      * @param {Number} offset
10 |      */
11 |     toBeNear(value: number, offset: number): R
12 |   }
13 | }
14 | 


--------------------------------------------------------------------------------
/examples/vite-react/.gitignore:
--------------------------------------------------------------------------------
 1 | # Logs
 2 | logs
 3 | *.log
 4 | npm-debug.log*
 5 | yarn-debug.log*
 6 | yarn-error.log*
 7 | pnpm-debug.log*
 8 | lerna-debug.log*
 9 | 
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 | 
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 | 


--------------------------------------------------------------------------------
/examples/angular/src/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |   
 5 |   mediainfo.js Angular example
 6 |   
 7 |   
 8 |   
 9 | 
10 | 
11 |   
12 | 
13 | 
14 | 


--------------------------------------------------------------------------------
/examples/vite-react/index.html:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 |   
 4 |     
 5 |     
 6 |     mediainfo.js Vite/React Example
 7 |   
 8 |   
 9 |     
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/error.ts: -------------------------------------------------------------------------------- 1 | function isError(error: unknown): error is Error { 2 | return ( 3 | error !== null && 4 | typeof error === 'object' && 5 | Object.prototype.hasOwnProperty.call(error, 'message') 6 | ) 7 | } 8 | 9 | function unknownToError(error: unknown): Error { 10 | if (isError(error)) { 11 | return error 12 | } 13 | return new Error(typeof error === 'string' ? error : 'Unknown error') 14 | } 15 | 16 | export { unknownToError } 17 | -------------------------------------------------------------------------------- /examples/webpack-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "baseUrl": ".", 5 | "esModuleInterop": true, 6 | "jsx": "react", 7 | "module": "ESNext", 8 | "moduleResolution": "node", 9 | "noImplicitAny": true, 10 | "outDir": "dist", 11 | "resolveJsonModule": true, 12 | "skipLibCheck": true, 13 | "sourceMap": true, 14 | "strict": true, 15 | "target": "ES6" 16 | }, 17 | "include": ["src", "webpack.config.js"] 18 | } 19 | -------------------------------------------------------------------------------- /gulp/index.ts: -------------------------------------------------------------------------------- 1 | export { default as mediainfolib } from './compile/mediainfolib.ts' 2 | export { default as wasm } from './compile/wasm.ts' 3 | export { default as zenlib } from './compile/zenlib.ts' 4 | export { default as declaration } from './declaration.ts' 5 | export { default } from './default.ts' 6 | export { default as download } from './download.ts' 7 | export { default as generateTypes } from './generate-types/generate.ts' 8 | export { default as babel } from './transpile/babel.ts' 9 | export { default as rollup } from './transpile/rollup.ts' 10 | -------------------------------------------------------------------------------- /__tests__/unicode.test.ts: -------------------------------------------------------------------------------- 1 | import { analyzeFile, expectToBeDefined, fixturePath } from './utils.ts' 2 | 3 | const filePath = fixturePath('sample.mkv') 4 | 5 | it('should return unicode data', async () => { 6 | const result = await analyzeFile(filePath, { full: true }) 7 | expectToBeDefined(result.media) 8 | 9 | const { track } = result.media 10 | const [track0] = track 11 | expect(track0.Title).toBe( 12 | 'Dès Noël où un zéphyr haï me vêt de glaçons würmiens je dîne ' + 13 | 'd’exquis rôtis de bœuf au kir à l’aÿ d’âge mûr & cætera !' 14 | ) 15 | }) 16 | -------------------------------------------------------------------------------- /__tests__/locateFile.test.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import mediaInfoFactory from '..' 4 | 5 | const distDir = path.resolve(import.meta.dirname, '..', 'dist', 'cjs') 6 | 7 | it('should use locateFile callback', async () => { 8 | const locateFile = jest.fn((filename: string, prefix: string) => 9 | path.resolve(prefix, '..', filename) 10 | ) 11 | const mi = await mediaInfoFactory({ locateFile }) 12 | expect(locateFile).toHaveBeenCalledTimes(1) 13 | expect(locateFile).toHaveBeenCalledWith('MediaInfoModule.wasm', `${distDir}/`) 14 | mi.close() 15 | }) 16 | -------------------------------------------------------------------------------- /examples/angular/eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals' 2 | import tsEslint from 'typescript-eslint' 3 | 4 | export default tsEslint.config( 5 | ...tsEslint.configs.strictTypeChecked, 6 | ...tsEslint.configs.stylisticTypeChecked, 7 | { 8 | files: ['**/*.{js,ts,tsx}'], 9 | languageOptions: { 10 | globals: globals.browser, 11 | parserOptions: { 12 | project: true, 13 | tsconfigRootDir: import.meta.dirname, 14 | }, 15 | }, 16 | }, 17 | { 18 | ignores: ['dist/*', 'eslint.config.js', 'vite.config.ts'], 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /__tests__/concurrentAnalysis.test.ts: -------------------------------------------------------------------------------- 1 | import mediaInfoFactory, { type ReadChunkFunc } from '..' 2 | 3 | const readChunk: ReadChunkFunc = async () => { 4 | await new Promise((r) => setTimeout(r, 50)) 5 | return new Uint8Array([1, 2, 3]) 6 | } 7 | 8 | it('should not allow multiple simultaneous analyzeData calls (issue #173)', async () => { 9 | const mi = await mediaInfoFactory() 10 | void mi.analyzeData(99_999, readChunk) 11 | 12 | await expect(mi.analyzeData(99_999, readChunk)).rejects.toThrow( 13 | /cannot start a new analysis while another is in progress/ 14 | ) 15 | }) 16 | -------------------------------------------------------------------------------- /patches/gulp-sourcemaps@3.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/src/utils.js b/src/utils.js 2 | index 33a0003d7293e502c508b63ef748aae77ca2754f..199564829258dd72e1167f0f67d1d0381ffa0364 100644 3 | --- a/src/utils.js 4 | +++ b/src/utils.js 5 | @@ -28,6 +28,9 @@ var commentFormatters = { 6 | js: function jsCommentFormatter(preLine, newline, url) { 7 | return preLine + '//# sourceMappingURL=' + url + newline; 8 | }, 9 | + cjs: function jsCommentFormatter(preLine, newline, url) { 10 | + return preLine + '//# sourceMappingURL=' + url + newline; 11 | + }, 12 | default: function defaultFormatter() { 13 | return ''; 14 | }, -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | FormatType, 3 | default as MediaInfo, 4 | ReadChunkFunc, 5 | ResultMap, 6 | SizeArg, 7 | } from './MediaInfo.js' 8 | export type { MediaInfoFactoryOptions } from './mediaInfoFactory.js' 9 | export { default, default as mediaInfoFactory } from './mediaInfoFactory.js' 10 | export type { 11 | AudioTrack, 12 | BaseTrack, 13 | CreationInfo, 14 | Extra, 15 | GeneralTrack, 16 | ImageTrack, 17 | Media, 18 | MediaInfoResult, 19 | MenuTrack, 20 | OtherTrack, 21 | TextTrack, 22 | Track, 23 | VideoTrack, 24 | } from './MediaInfoResult.js' 25 | export { isTrackType } from './typeGuard.js' 26 | -------------------------------------------------------------------------------- /src/typeGuard.ts: -------------------------------------------------------------------------------- 1 | import type { Track } from './MediaInfoResult' 2 | 3 | /** 4 | * Checks if a given object is of a specified track type. 5 | * 6 | * @template T - The type of track to check for. 7 | * @param thing - The object to check. 8 | * @param type - The track type to check against. 9 | * @returns A boolean indicating whether the object is of the specified track type. 10 | */ 11 | function isTrackType( 12 | thing: unknown, 13 | type: T 14 | ): thing is Extract { 15 | return thing !== null && typeof thing === 'object' && (thing as Track)['@type'] === type 16 | } 17 | 18 | export { isTrackType } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowImportingTsExtensions": true, 4 | "allowJs": true, 5 | "esModuleInterop": true, 6 | "module": "es2022", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "target": "es2022", 12 | "types": ["@types/emscripten", "@types/node"] 13 | }, 14 | "include": [ 15 | "__tests__/**/*", 16 | "babel.config.cjs", 17 | "eslint.config.js", 18 | "gulp/**/*", 19 | "gulpfile.ts", 20 | "rollup.config.js", 21 | "src/**/*.ts" 22 | ], 23 | "exclude": ["build/**/*", "dist/**/*", "examples/**/*", "node_modules/**/*"] 24 | } 25 | -------------------------------------------------------------------------------- /examples/vite-react/README.md: -------------------------------------------------------------------------------- 1 | # mediainfo.js Vite + React Example 2 | 3 | This example shows how to use `mediainfo.js` in a Vite + React project, with proper handling of the WebAssembly (WASM) file. 4 | 5 | ## WebAssembly Handling 6 | 7 | The `MediaInfoModule.wasm` file is copied into the build output using `vite-plugin-static-copy` to ensure it's available at runtime: 8 | 9 | ```js 10 | viteStaticCopy({ 11 | targets: [ 12 | { 13 | src: path.join( 14 | import.meta.dirname, 15 | 'node_modules', 16 | 'mediainfo.js', 17 | 'dist', 18 | 'MediaInfoModule.wasm' 19 | ), 20 | dest: '', 21 | }, 22 | ], 23 | }), 24 | ``` 25 | -------------------------------------------------------------------------------- /examples/vite-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"] 24 | } 25 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to mediainfo.js 2 | 3 | Thank you for considering to help mediainfo.js. :+1: 4 | 5 | When contributing to this repository, please first discuss the change you wish 6 | to make via an issue. Code changes are proposed via [Pull 7 | Requests.](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests) 8 | 9 | ## Using the issue tracker 10 | 11 | The [issue tracker](https://github.com/buzz/mediainfo.js/issues) is the main 12 | tool for [bug reports](#bugs) and [features requests](#features). 13 | **Please do not use the issue tracker for support.** Use [Stack Overflow](https://stackoverflow.com/search?q=mediainfo.js) for user questions. 14 | -------------------------------------------------------------------------------- /gulp/default.ts: -------------------------------------------------------------------------------- 1 | import gulp from 'gulp' 2 | 3 | import mediainfolib from './compile/mediainfolib.ts' 4 | import wasm from './compile/wasm.ts' 5 | import zenlib from './compile/zenlib.ts' 6 | import declaration from './declaration.ts' 7 | import download from './download.ts' 8 | import generateTypes from './generate-types/generate.ts' 9 | import babel from './transpile/babel.ts' 10 | import rollup from './transpile/rollup.ts' 11 | 12 | const defaultTask = gulp.series([ 13 | gulp.parallel([generateTypes, gulp.series([download, zenlib, mediainfolib, wasm])]), 14 | gulp.parallel([babel, declaration, rollup]), 15 | ]) 16 | defaultTask.displayName = 'default' 17 | defaultTask.description = 'Build project' 18 | 19 | export default defaultTask 20 | -------------------------------------------------------------------------------- /__tests__/many_tracks.test.ts: -------------------------------------------------------------------------------- 1 | import { analyzeFile, expectToBeDefined, expectTrackType, fixturePath } from './utils' 2 | 3 | it('should parse file with many tracks', async () => { 4 | const result = await analyzeFile(fixturePath('many_tracks.mp4')) 5 | expectToBeDefined(result.media) 6 | 7 | const { track } = result.media 8 | expect(track).toHaveLength(102) 9 | 10 | const [track0, track1] = track 11 | expectTrackType(track0, 'General') 12 | expect(track0.Format).toBe('MPEG-4') 13 | expect(track0.FileSize).toBe('208534') 14 | 15 | expectTrackType(track1, 'Video') 16 | expect(track1.Format).toBe('AVC') 17 | 18 | const track101 = track[101] 19 | expectTrackType(track101, 'Text') 20 | expect(track[101].Format).toBe('Timed Text') 21 | }) 22 | -------------------------------------------------------------------------------- /examples/webpack-react/eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals' 2 | import reactJsxRuntime from 'eslint-plugin-react/configs/jsx-runtime.js' 3 | import tsEslint from 'typescript-eslint' 4 | 5 | export default tsEslint.config( 6 | ...tsEslint.configs.strictTypeChecked, 7 | ...tsEslint.configs.stylisticTypeChecked, 8 | { 9 | files: ['**/*.{js,ts,tsx}'], 10 | ...reactJsxRuntime, 11 | languageOptions: { 12 | ...reactJsxRuntime.languageOptions, 13 | globals: globals.browser, 14 | parserOptions: { 15 | project: true, 16 | tsconfigRootDir: import.meta.dirname, 17 | }, 18 | }, 19 | settings: { 20 | react: { version: 'detect' }, 21 | }, 22 | }, 23 | { 24 | ignores: ['dist/*', 'eslint.config.js'], 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /examples/vite-react/eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from 'globals' 2 | import reactJsxRuntime from 'eslint-plugin-react/configs/jsx-runtime.js' 3 | import tsEslint from 'typescript-eslint' 4 | 5 | export default tsEslint.config( 6 | ...tsEslint.configs.strictTypeChecked, 7 | ...tsEslint.configs.stylisticTypeChecked, 8 | { 9 | files: ['**/*.{js,ts,tsx}'], 10 | ...reactJsxRuntime, 11 | languageOptions: { 12 | ...reactJsxRuntime.languageOptions, 13 | globals: globals.browser, 14 | parserOptions: { 15 | project: true, 16 | tsconfigRootDir: import.meta.dirname, 17 | }, 18 | }, 19 | settings: { 20 | react: { version: 'detect' }, 21 | }, 22 | }, 23 | { 24 | ignores: ['dist/*', 'eslint.config.js', 'vite.config.ts'], 25 | } 26 | ) 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🚀 Feature request' 3 | about: Suggest an idea for improving mediainfo.js 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | ### Is your proposal related to a problem? 10 | 11 | 15 | 16 | ### Describe the solution you'd like 17 | 18 | 21 | 22 | ### Additional context 23 | 24 | 28 | 29 | ### Contribution 30 | 31 | 34 | -------------------------------------------------------------------------------- /examples/vite-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediainfojs-vite-react-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint .", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "mediainfo.js": "link:../..", 14 | "react": "^19.1.0", 15 | "react-dom": "^19.1.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^19.1.2", 19 | "@types/react-dom": "^19.1.2", 20 | "@vitejs/plugin-react": "^4.4.0", 21 | "eslint": "^9.24.0", 22 | "eslint-plugin-react": "^7.37.5", 23 | "globals": "^16.0.0", 24 | "typescript": "^5.8.3", 25 | "typescript-eslint": "^8.30.1", 26 | "vite": "^6.3.2", 27 | "vite-plugin-static-copy": "^2.3.1" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /__tests__/args.test.ts: -------------------------------------------------------------------------------- 1 | import mediaInfoFactory from '..' 2 | import { expectToBeDefined, expectTrackType } from './utils' 3 | import type { MediaInfo, SizeArg } from '..' 4 | 5 | it.each([ 6 | ['number', 20], 7 | ['function', () => 20], 8 | ['async function', () => Promise.resolve(20)], 9 | ])('should accept %s as size arg', async (_: string, size: SizeArg) => { 10 | let mi: MediaInfo | undefined 11 | 12 | try { 13 | mi = await mediaInfoFactory() 14 | const result = await mi.analyzeData(size, () => new Uint8Array(10)) 15 | expectToBeDefined(result.media) 16 | const { track } = result.media 17 | expect(track).toHaveLength(1) 18 | const [track0] = track 19 | expectTrackType(track0, 'General') 20 | expect(track0.FileSize).toBe('20') 21 | } finally { 22 | if (mi) { 23 | mi.close() 24 | } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /gulp/compile/zenlib.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import { CPU_CORES, CXXFLAGS, VENDOR_DIR } from '../constants.ts' 4 | import { spawn } from '../utils.ts' 5 | 6 | const zenlibDir = path.join(VENDOR_DIR, 'ZenLib', 'Project', 'GNU', 'Library') 7 | 8 | async function task() { 9 | await spawn('./autogen.sh', [], zenlibDir) 10 | await spawn('sed', ['-i', 's/-O2/-Oz/', 'configure'], zenlibDir) 11 | await spawn( 12 | 'emconfigure', 13 | [ 14 | './configure', 15 | '--host=le32-unknown-nacl', 16 | '--disable-unicode', 17 | '--enable-static', 18 | '--disable-shared', 19 | `CXXFLAGS=${CXXFLAGS}`, 20 | ], 21 | zenlibDir 22 | ) 23 | await spawn('emmake', ['make', `-j${CPU_CORES}`], zenlibDir) 24 | } 25 | 26 | task.displayName = 'compile:zenlib' 27 | task.description = 'Compile zenlib' 28 | 29 | export default task 30 | -------------------------------------------------------------------------------- /.github/workflows/close_inactive_issues.yml: -------------------------------------------------------------------------------- 1 | name: Close inactive issues 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | jobs: 7 | close-issues: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/stale@v5 14 | with: 15 | days-before-issue-stale: 30 16 | days-before-issue-close: 30 17 | stale-issue-label: 'stale' 18 | stale-issue-message: 'This issue is stale because it has been open for 30 days with no activity.' 19 | close-issue-message: 'This issue was closed because it has been inactive for 30 days since being marked as stale.' 20 | days-before-pr-stale: -1 21 | days-before-pr-close: -1 22 | repo-token: ${{ secrets.GITHUB_TOKEN }} 23 | exempt-issue-labels: 'disable-stale' 24 | -------------------------------------------------------------------------------- /gulp/declaration.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import gulp from 'gulp' 4 | 5 | import { DIST_DIR, PROJECT_DIR, SRC_DIR } from './constants.ts' 6 | import { spawn } from './utils.ts' 7 | 8 | async function generateDeclaration() { 9 | const args = [ 10 | '--emitDeclarationOnly', 11 | '--declarationDir', 12 | DIST_DIR, 13 | '--declaration', 14 | 'true', 15 | '--skipLibCheck', 16 | path.join(SRC_DIR, 'index.ts'), 17 | ] 18 | await spawn('tsc', args, PROJECT_DIR) 19 | } 20 | 21 | function copyDeclaration() { 22 | return gulp.src(path.join(SRC_DIR, 'MediaInfoModule.d.ts')).pipe(gulp.dest(DIST_DIR)) 23 | } 24 | 25 | const declarationTask = gulp.parallel([generateDeclaration, copyDeclaration]) 26 | declarationTask.displayName = 'declaration' 27 | declarationTask.description = 'Generate TS declaration' 28 | 29 | export default declarationTask 30 | -------------------------------------------------------------------------------- /examples/browser-umd/example.js: -------------------------------------------------------------------------------- 1 | const fileinput = document.getElementById('fileinput') 2 | const output = document.getElementById('output') 3 | 4 | const onChangeFile = (mediainfo) => { 5 | const file = fileinput.files[0] 6 | if (file) { 7 | output.value = 'Working…' 8 | 9 | const readChunk = async (chunkSize, offset) => 10 | new Uint8Array(await file.slice(offset, offset + chunkSize).arrayBuffer()) 11 | 12 | mediainfo 13 | .analyzeData(file.size, readChunk) 14 | .then((result) => { 15 | output.value = result 16 | }) 17 | .catch((error) => { 18 | output.value = `An error occured:\n${error.stack}` 19 | }) 20 | } 21 | } 22 | 23 | MediaInfo.mediaInfoFactory({ format: 'text' }, (mediainfo) => { 24 | fileinput.removeAttribute('disabled') 25 | fileinput.addEventListener('change', () => onChangeFile(mediainfo)) 26 | }) 27 | -------------------------------------------------------------------------------- /examples/webpack-react/README.md: -------------------------------------------------------------------------------- 1 | # mediainfo.js Webpack + React Example 2 | 3 | This minimal example demonstrates how to integrate `mediainfo.js` with a Webpack + React setup. It focuses on correctly handling the WebAssembly (WASM) file so Webpack can locate and load it properly. 4 | 5 | ## Webpack Configuration 6 | 7 | To ensure the WASM file is handled correctly, two adjustments are necessary in `webpack.config.js`: 8 | 9 | 1. **Preserve the original WASM filename** 10 | 11 | ```js 12 | assetModuleFilename: '[name][ext]', 13 | ``` 14 | 15 | 2. **Make the WASM file discoverable via alias** 16 | 17 | ```js 18 | alias: { 'MediaInfoModule.wasm': wasmFilePath }, 19 | ``` 20 | 21 | ## Loading the WASM in React 22 | 23 | In `App.tsx`, override the `locateFile` function when calling `mediaInfoFactory` to instruct the browser where to find the WASM file: 24 | 25 | ```js 26 | locateFile: (filename) => filename, 27 | ``` 28 | -------------------------------------------------------------------------------- /examples/vite-react/vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig, searchForWorkspaceRoot } from 'vite' 3 | import react from '@vitejs/plugin-react' 4 | import { viteStaticCopy } from 'vite-plugin-static-copy' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | react(), 10 | viteStaticCopy({ 11 | targets: [ 12 | { 13 | src: path.join( 14 | import.meta.dirname, 15 | 'node_modules', 16 | 'mediainfo.js', 17 | 'dist', 18 | 'MediaInfoModule.wasm' 19 | ), 20 | dest: '', 21 | }, 22 | ], 23 | }), 24 | ], 25 | server: { 26 | fs: { 27 | allow: [ 28 | // search up for workspace root 29 | searchForWorkspaceRoot(process.cwd()), 30 | // allow wasm file 31 | '../../dist/MediaInfoModule.wasm', 32 | ], 33 | }, 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /gulp/compile/mediainfolib.ts: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | 3 | import { CPU_CORES, CXXFLAGS, MediaInfoLib_CXXFLAGS, VENDOR_DIR } from '../constants.ts' 4 | import { spawn } from '../utils.ts' 5 | 6 | const mediainfolibDir = path.join(VENDOR_DIR, 'MediaInfoLib', 'Project', 'GNU', 'Library') 7 | 8 | async function task() { 9 | await spawn('./autogen.sh', [], mediainfolibDir) 10 | await spawn('sed', ['-i', 's/-O2/-Oz/', 'configure'], mediainfolibDir) 11 | await spawn( 12 | 'emconfigure', 13 | [ 14 | './configure', 15 | '--host=le32-unknown-nacl', 16 | '--enable-static', 17 | '--disable-shared', 18 | '--disable-dll', 19 | `CXXFLAGS=${CXXFLAGS} ${MediaInfoLib_CXXFLAGS}`, 20 | ], 21 | mediainfolibDir 22 | ) 23 | await spawn('emmake', ['make', `-j${CPU_CORES}`], mediainfolibDir) 24 | } 25 | 26 | task.displayName = 'compile:mediainfolib' 27 | task.description = 'Compile MediaInfoLib' 28 | 29 | export default task 30 | -------------------------------------------------------------------------------- /__tests__/error.readChunk.test.ts: -------------------------------------------------------------------------------- 1 | import mediaInfoFactory, { type MediaInfo } from '..' 2 | import { expectToBeError } from './utils.ts' 3 | 4 | const getSize = () => 99 5 | const readChunk = () => { 6 | throw new Error('Foo error') 7 | } 8 | 9 | it('should return error via callback', async () => { 10 | expect.assertions(2) 11 | let mi: MediaInfo | undefined 12 | 13 | try { 14 | mi = await mediaInfoFactory() 15 | mi.analyzeData(getSize, readChunk, (result, error) => { 16 | expectToBeError(error) 17 | expect(error.message).toBe('Foo error') 18 | }) 19 | } finally { 20 | if (mi) { 21 | mi.close() 22 | } 23 | } 24 | }) 25 | 26 | it('should return error via Promise', async () => { 27 | expect.assertions(1) 28 | let mi: MediaInfo | undefined 29 | 30 | try { 31 | mi = await mediaInfoFactory() 32 | await expect(mi.analyzeData(getSize, readChunk)).rejects.toThrow('Foo error') 33 | } finally { 34 | if (mi) { 35 | mi.close() 36 | } 37 | } 38 | }) 39 | -------------------------------------------------------------------------------- /examples/angular/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "outDir": "./dist/out-tsc", 6 | "strict": true, 7 | "noImplicitOverride": true, 8 | "noPropertyAccessFromIndexSignature": true, 9 | "noImplicitReturns": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "bundler", 17 | "importHelpers": true, 18 | "target": "ES2022", 19 | "module": "ES2022", 20 | "useDefineForClassFields": false, 21 | "lib": [ 22 | "ES2022", 23 | "dom" 24 | ] 25 | }, 26 | "angularCompilerOptions": { 27 | "enableI18nLegacyMessageIdFormat": false, 28 | "strictInjectionParameters": true, 29 | "strictInputAccessModifiers": true, 30 | "strictTemplates": true 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/angular/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build", 9 | "watch": "ng build --watch --configuration development", 10 | "lint": "eslint src/" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "^18.0.0", 15 | "@angular/common": "^18.0.0", 16 | "@angular/compiler": "^18.0.0", 17 | "@angular/core": "^18.0.0", 18 | "@angular/forms": "^18.0.0", 19 | "@angular/platform-browser": "^18.0.0", 20 | "@angular/platform-browser-dynamic": "^18.0.0", 21 | "@angular/router": "^18.0.0", 22 | "mediainfo.js": "link:../..", 23 | "rxjs": "~7.8.0", 24 | "tslib": "^2.3.0", 25 | "zone.js": "~0.14.3" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "^18.0.2", 29 | "@angular/cli": "^18.0.2", 30 | "@angular/compiler-cli": "^18.0.0", 31 | "globals": "^15.3.0", 32 | "typescript": "~5.4.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/MediaInfoModule.d.ts: -------------------------------------------------------------------------------- 1 | import type { FORMAT_CHOICES } from './MediaInfo.js' 2 | 3 | type WasmConstructableFormatType = (typeof FORMAT_CHOICES)[number] 4 | 5 | interface MediaInfoWasmInterface { 6 | delete(): void 7 | close(): void 8 | inform(): string 9 | open_buffer_continue(data: Uint8Array, size: number): number 10 | open_buffer_continue_goto_get_lower(): number 11 | open_buffer_continue_goto_get_upper(): number 12 | open_buffer_finalize(): number 13 | open_buffer_init(estimatedFileSize: number, fileOffset: number): number 14 | } 15 | 16 | type MediaInfoWasmConstructable = new ( 17 | format: WasmConstructableFormatType, 18 | coverData: boolean, 19 | full: boolean 20 | ) => MediaInfoWasmInterface 21 | 22 | interface MediaInfoModule extends EmscriptenModule { 23 | MediaInfo: MediaInfoWasmConstructable 24 | } 25 | 26 | declare const mediaInfoModuleFactory: EmscriptenModuleFactory 27 | 28 | export type { MediaInfoModule, MediaInfoWasmConstructable, MediaInfoWasmInterface } 29 | export default mediaInfoModuleFactory 30 | -------------------------------------------------------------------------------- /__tests__/instantiation.test.ts: -------------------------------------------------------------------------------- 1 | import mediaInfoFactory, { type MediaInfo } from '..' 2 | 3 | const methodNames = [ 4 | 'analyzeData', 5 | 'close', 6 | 'inform', 7 | 'openBufferContinue', 8 | 'openBufferFinalize', 9 | 'openBufferContinueGotoGet', 10 | 'openBufferInit', 11 | 'reset', 12 | ] as const 13 | 14 | const expectMediainfoObj = (mi: MediaInfo) => { 15 | for (const name of methodNames) { 16 | expect(mi[name]).toBeInstanceOf(Function) 17 | } 18 | expect(mi.options.chunkSize).toEqual(expect.any(Number)) 19 | } 20 | 21 | it('should instantiate via callback', (done) => { 22 | mediaInfoFactory({}, (mi) => { 23 | try { 24 | expectMediainfoObj(mi) 25 | } finally { 26 | mi.close() 27 | done() 28 | } 29 | }) 30 | }) 31 | 32 | it('should instantiate via Promise', async () => { 33 | expect.assertions(9) 34 | let mi: MediaInfo | undefined 35 | try { 36 | mi = await mediaInfoFactory() 37 | expectMediainfoObj(mi) 38 | } finally { 39 | if (mi) { 40 | mi.close() 41 | } 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /__tests__/AudioVideoInterleave.avi.test.ts: -------------------------------------------------------------------------------- 1 | import { analyzeFile, expectToBeDefined, expectTrackType, fixturePath } from './utils.ts' 2 | 3 | it('should parse file', async () => { 4 | const result = await analyzeFile(fixturePath('AudioVideoInterleave.avi')) 5 | expectToBeDefined(result.media) 6 | 7 | const { track } = result.media 8 | expect(track).toHaveLength(2) 9 | const [track0, track1] = track 10 | 11 | expectTrackType(track0, 'General') 12 | expect(track0.Format).toBe('AVI') 13 | expect(track0.FileSize).toBe('5686') 14 | expect(track0.Encoded_Application).toBe('Lavf57.41.100') 15 | 16 | expectTrackType(track1, 'Video') 17 | expect(track1.Format).toBe('MPEG-4 Visual') 18 | expect(track1.CodecID).toBe('FMP4') 19 | expect(track1.Height).toBe(1) 20 | expect(track1.Width).toBe(1) 21 | expect(track1.DisplayAspectRatio).toBeCloseTo(1) 22 | expect(track1.FrameRate).toBeCloseTo(100) 23 | expect(track1.ColorSpace).toBe('YUV') 24 | expect(track1.ChromaSubsampling).toBe('4:2:0') 25 | expect(track1.Compression_Mode).toBe('Lossy') 26 | }) 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | ffmpeg.wasm 4 | 5 |

6 | 7 | # mediainfo.js 8 | 9 | mediainfo.js is a web-compatible version of the [MediaInfoLib](https://mediaarea.net/en/MediaInfo), originally written in C++. Compiled 10 | from C++ to WebAssembly, mediainfo.js enables browser compatibility while also supporting Node.js 11 | execution. 12 | 13 | ## Live Demo 14 | 15 | [Try mediainfo.js in your browser.](https://mediainfo.js.org/demo) 16 | 17 | ## Documentation 18 | 19 | - [Introduction](https://mediainfo.js.org/docs/intro/) 20 | - [Getting Started](https://mediainfo.js.org/docs/getting-started/installation/) 21 | - [API](https://mediainfo.js.org/api/) 22 | 23 | ## License 24 | 25 | This program is freeware under BSD-2-Clause license conditions: 26 | [MediaInfo(Lib) License](https://mediaarea.net/en/MediaInfo/License) 27 | 28 | This product uses [MediaInfo](https://mediaarea.net/en/MediaInfo) library, 29 | Copyright (c) 2002-2023 [MediaArea.net SARL](mailto:Info@MediaArea.net). 30 | -------------------------------------------------------------------------------- /examples/browser-umd/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | mediainfo.js simple demo 7 | 31 | 32 | 33 |
34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /examples/webpack-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mediainfojs-webpack-react-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "cross-env NODE_ENV=development webpack serve", 8 | "build": "cross-env NODE_ENV=production webpack", 9 | "serve": "cross-env NODE_ENV=production webpack && cross-env serve dist/", 10 | "lint": "eslint src/" 11 | }, 12 | "devDependencies": { 13 | "@pmmmwh/react-refresh-webpack-plugin": "^0.5.16", 14 | "@types/node": "^22.14.1", 15 | "@types/react": "^19.1.2", 16 | "@types/react-dom": "^19.1.2", 17 | "@types/webpack-env": "^1.18.8", 18 | "cross-env": "^7.0.3", 19 | "eslint": "^9.24.0", 20 | "eslint-plugin-react": "^7.37.5", 21 | "fork-ts-checker-webpack-plugin": "^9.1.0", 22 | "globals": "^16.0.0", 23 | "html-webpack-plugin": "^5.6.3", 24 | "react-refresh": "^0.17.0", 25 | "serve": "^14.2.4", 26 | "ts-loader": "9.5.2", 27 | "typescript": "^5.8.3", 28 | "typescript-eslint": "^8.30.1", 29 | "webpack": "^5.99.5", 30 | "webpack-cli": "^6.0.1", 31 | "webpack-dev-server": "^5.2.1" 32 | }, 33 | "dependencies": { 34 | "mediainfo.js": "link:../..", 35 | "react": "^19.1.0", 36 | "react-dom": "^19.1.0" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2002-2016 MediaArea.net SARL, All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 14 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 15 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 17 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 18 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 19 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 20 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 21 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 22 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /__tests__/error.wasmLoading.test.ts: -------------------------------------------------------------------------------- 1 | import mediaInfoFactory from '..' 2 | import { expectToBeError } from './utils' 3 | 4 | beforeEach(() => { 5 | // Suppress console output from emscripten module 6 | jest.spyOn(console, 'error') 7 | // @ts-expect-error TS doesn't know mockImplementation 8 | // eslint-disable-next-line @typescript-eslint/no-unsafe-call 9 | console.error.mockImplementation(() => null) 10 | }) 11 | 12 | afterEach(() => { 13 | jest.restoreAllMocks() 14 | }) 15 | 16 | describe('Error on WASM loading', () => { 17 | it('should return error via callback and throw Exception', (done) => { 18 | mediaInfoFactory( 19 | { locateFile: () => 'file_does_not_exist.wasm' }, 20 | () => { 21 | done('Resolve callback should not fire') 22 | }, 23 | (error) => { 24 | expectToBeError(error) 25 | expect(error.message).toMatch('no such file') 26 | done() 27 | } 28 | ) 29 | }) 30 | 31 | it('should return error via Promise and throw Exception', (done) => { 32 | mediaInfoFactory({ locateFile: () => 'file_does_not_exist.wasm' }) 33 | .then(() => { 34 | done('Resolve callback should not fire') 35 | }) 36 | .catch((error: unknown) => { 37 | expectToBeError(error) 38 | expect(error.message).toMatch('no such file') 39 | done() 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /__tests__/jest/toBeNear.ts: -------------------------------------------------------------------------------- 1 | // toBeNear by offset match from 2 | // https://github.com/jest-community/jest-extended/pull/183/commits/dca63a359c9856c5c761cfc1a598f4d7e583978e 3 | 4 | import { matcherHint, printExpected, printReceived } from 'jest-matcher-utils' 5 | 6 | const passMessage = (received: number, value: number, offset: number) => () => 7 | matcherHint('.not.toBeNear', 'received', 'value', { secondArgument: 'offset' }) + 8 | '\n\n' + 9 | `Value: ${printExpected(value)}\n` + 10 | `Offset: ${printExpected(offset)}\n` + 11 | `Interval: [${printExpected(value - offset)}, ${printExpected(value + offset)}]\n` + 12 | `Received: ${printReceived(received)}` 13 | 14 | const failMessage = (received: number, value: number, offset: number) => () => 15 | matcherHint('.toBeNear', 'received', 'value', { secondArgument: 'offset' }) + 16 | '\n\n' + 17 | `Value: ${printExpected(value)}\n` + 18 | `Offset: ${printExpected(offset)}\n` + 19 | `Interval: [${printExpected(value - offset)}, ${printExpected(value + offset)}]\n` + 20 | `Received: ${printReceived(received)}` 21 | 22 | expect.extend({ 23 | toBeNear: (received: number, value: number, offset: number) => { 24 | const pass = Math.abs(received - value) <= offset 25 | if (pass) { 26 | return { pass: true, message: passMessage(received, value, offset) } 27 | } 28 | return { pass: false, message: failMessage(received, value, offset) } 29 | }, 30 | }) 31 | -------------------------------------------------------------------------------- /examples/angular/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | 3 | import mediaInfoFactory from 'mediainfo.js' 4 | import type { MediaInfo, ReadChunkFunc } from 'mediainfo.js' 5 | 6 | function makeReadChunk(file: File): ReadChunkFunc { 7 | return async (chunkSize: number, offset: number) => 8 | new Uint8Array(await file.slice(offset, offset + chunkSize).arrayBuffer()) 9 | } 10 | 11 | @Component({ 12 | selector: 'app-root', 13 | standalone: true, 14 | templateUrl: './app.component.html', 15 | }) 16 | export class AppComponent { 17 | mediaInfo: MediaInfo<'text'> | undefined = undefined 18 | result = '' 19 | disabled = true 20 | 21 | constructor() { 22 | mediaInfoFactory({ format: 'text' }) 23 | .then((mediaInfo) => { 24 | this.mediaInfo = mediaInfo 25 | this.disabled = false 26 | }) 27 | .catch((error: unknown) => { 28 | console.error(error) 29 | }) 30 | } 31 | 32 | onChangeFile(event: Event) { 33 | if (this.mediaInfo && event.target) { 34 | const target = event.target as HTMLInputElement 35 | if (target.files && target.files.length > 0) { 36 | const file = target.files[0] 37 | this.mediaInfo 38 | .analyzeData(file.size, makeReadChunk(file)) 39 | .then((result) => { 40 | this.result = result 41 | }) 42 | .catch((error: unknown) => { 43 | console.error(error) 44 | }) 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /gulp/download.ts: -------------------------------------------------------------------------------- 1 | import { access, mkdir } from 'node:fs/promises' 2 | import path from 'node:path' 3 | 4 | import decompress from 'decompress' 5 | import gulp from 'gulp' 6 | 7 | import { LIBMEDIAINFO_VERSION, LIBZEN_VERSION, VENDOR_DIR } from './constants.ts' 8 | import { downloadFileToDir } from './utils.ts' 9 | 10 | const urls = { libmediainfo: LIBMEDIAINFO_VERSION, libzen: LIBZEN_VERSION } 11 | 12 | function getUrl(libname: string, version: string) { 13 | return `https://mediaarea.net/download/source/${libname}/${version}/${libname}_${version}.tar.bz2` 14 | } 15 | 16 | const task = gulp.parallel( 17 | Object.entries(urls).map(([libname, version]) => { 18 | const dlTask = async () => { 19 | await mkdir(VENDOR_DIR, { recursive: true }) 20 | const dlUrl = getUrl(libname, version) 21 | const { pathname } = new URL(dlUrl) 22 | const filename = path.basename(pathname) 23 | const filepath = path.join(VENDOR_DIR, filename) 24 | 25 | // skip download if file exists 26 | try { 27 | await access(filepath) 28 | } catch { 29 | await downloadFileToDir(dlUrl, filepath) 30 | } 31 | 32 | await decompress(filepath, VENDOR_DIR) 33 | } 34 | 35 | dlTask.displayName = `download:${libname}-v${version}` 36 | dlTask.Description = `Download ${libname} v${version} sources` 37 | 38 | return dlTask 39 | }) 40 | ) 41 | 42 | task.displayName = 'download' 43 | task.description = 'Download sources' 44 | 45 | export default task 46 | -------------------------------------------------------------------------------- /examples/vite-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { type ChangeEvent, useState, useEffect, useRef } from 'react' 2 | 3 | import mediaInfoFactory from 'mediainfo.js' 4 | import type { MediaInfo, ReadChunkFunc } from 'mediainfo.js' 5 | 6 | function makeReadChunk(file: File): ReadChunkFunc { 7 | return async (chunkSize: number, offset: number) => 8 | new Uint8Array(await file.slice(offset, offset + chunkSize).arrayBuffer()) 9 | } 10 | 11 | function App() { 12 | const mediaInfoRef = useRef | null>(null) 13 | const [result, setResult] = useState('') 14 | 15 | useEffect(() => { 16 | mediaInfoFactory({ format: 'text' }) 17 | .then((mi) => { 18 | mediaInfoRef.current = mi 19 | }) 20 | .catch((error: unknown) => { 21 | console.error(error) 22 | }) 23 | 24 | return () => { 25 | if (mediaInfoRef.current) { 26 | mediaInfoRef.current.close() 27 | } 28 | } 29 | }, []) 30 | 31 | const handleChange = (ev: ChangeEvent) => { 32 | const file = ev.target.files?.[0] 33 | if (file && mediaInfoRef.current) { 34 | mediaInfoRef.current 35 | .analyzeData(file.size, makeReadChunk(file)) 36 | .then(setResult) 37 | .catch((error: unknown) => { 38 | console.error(error) 39 | }) 40 | } 41 | } 42 | 43 | return ( 44 | <> 45 | 46 |
{result}
47 | 48 | ) 49 | } 50 | 51 | export default App 52 | -------------------------------------------------------------------------------- /__tests__/freeMXF-mxf1.mxf.test.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | 3 | import mediaInfoFactory from '..' 4 | import { expectToBeDefined, expectTrackType, fixturePath } from './utils' 5 | 6 | const makeReadChunk = (fh: fs.FileHandle) => async (chunkSize: number, offset: number) => { 7 | const buffer = new Uint8Array(chunkSize) 8 | await fh.read(buffer, 0, chunkSize, offset) 9 | return buffer 10 | } 11 | 12 | it('should parse MXF correctly on second run (issue #177)', async () => { 13 | const mi = await mediaInfoFactory() 14 | 15 | for (let i = 0; i < 2; ++i) { 16 | const fileHandle = await fs.open(fixturePath('freeMXF-mxf1.mxf'), 'r') 17 | const { size } = await fileHandle.stat() 18 | 19 | const result = await mi.analyzeData(size, makeReadChunk(fileHandle)) 20 | 21 | expectToBeDefined(result.media) 22 | const { track } = result.media 23 | 24 | const [track0, track1, ,] = track 25 | expectTrackType(track0, 'General') 26 | expect(track0.Format).toBe('MXF') 27 | expect(track0.Duration).toBe(10.64) 28 | expect(track0.OverallBitRate).toBe(2_116_833) 29 | expect(track0.FrameCount).toBe(266) 30 | expect(track0.StreamSize).toBe(26_112) 31 | expect(track0.FooterSize).toBe(4439) 32 | expect(track0.Encoded_Date).toBe('2006-01-11 18:58:47.524') 33 | 34 | expectTrackType(track1, 'Video') 35 | expect(track1.Format_Settings_GOP).toBe('M=3, N=12') 36 | expect(track1.Duration).toBe(10.64) 37 | expect(track1.FrameCount).toBe(266) 38 | expect(track1.Delay).toBeNear(3600, 5) // Still slightly off 🤷 39 | expect(track1.StreamSize).toBe(2_789_276) 40 | 41 | await fileHandle.close() 42 | } 43 | }) 44 | -------------------------------------------------------------------------------- /examples/webpack-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ChangeEvent, useState, useEffect, useRef } from 'react' 2 | 3 | import mediaInfoFactory from 'mediainfo.js' 4 | import type { ReadChunkFunc, MediaInfo } from 'mediainfo.js' 5 | 6 | function makeReadChunk(file: File): ReadChunkFunc { 7 | return async (chunkSize: number, offset: number) => 8 | new Uint8Array(await file.slice(offset, offset + chunkSize).arrayBuffer()) 9 | } 10 | 11 | function App() { 12 | const mediaInfoRef = useRef | null>(null) 13 | const [result, setResult] = useState('') 14 | 15 | useEffect(() => { 16 | mediaInfoFactory({ 17 | format: 'text', 18 | // IMPORTANT: load the wasm file from same directory 19 | locateFile: (filename) => filename, 20 | }) 21 | .then((mi) => { 22 | mediaInfoRef.current = mi 23 | }) 24 | .catch((error: unknown) => { 25 | console.error(error) 26 | }) 27 | 28 | return () => { 29 | if (mediaInfoRef.current) { 30 | mediaInfoRef.current.close() 31 | } 32 | } 33 | }, []) 34 | 35 | const handleChange = (ev: ChangeEvent) => { 36 | const file = ev.target.files?.[0] 37 | if (file && mediaInfoRef.current) { 38 | mediaInfoRef.current 39 | .analyzeData(file.size, makeReadChunk(file)) 40 | .then(setResult) 41 | .catch((error: unknown) => { 42 | console.error(error) 43 | }) 44 | } 45 | } 46 | 47 | return ( 48 | <> 49 | 50 |
{result}
51 | 52 | ) 53 | } 54 | 55 | export default App 56 | -------------------------------------------------------------------------------- /examples/webpack-react/webpack.config.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path' 2 | import HtmlWebpackPlugin from 'html-webpack-plugin' 3 | import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin' 4 | import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin' 5 | 6 | const isDev = process.env.NODE_ENV === 'development' 7 | 8 | // IMPORTANT: help webpack find the wasm file 9 | const wasmFilePath = path.resolve( 10 | import.meta.dirname, 11 | 'node_modules', 12 | 'mediainfo.js', 13 | 'dist', 14 | 'MediaInfoModule.wasm' 15 | ) 16 | 17 | export default { 18 | mode: process.env.NODE_ENV, 19 | entry: './src/main.tsx', 20 | module: { 21 | rules: [ 22 | { 23 | test: /\.tsx?$/, 24 | exclude: /node_modules/, 25 | use: { 26 | loader: 'ts-loader', 27 | options: { transpileOnly: true }, 28 | }, 29 | }, 30 | ], 31 | }, 32 | output: { 33 | path: path.resolve(import.meta.dirname, 'dist'), 34 | filename: isDev ? '[name].js' : '[name].[contenthash].js', 35 | // IMPORTANT: ensure the wasm filename doesn't get mangled by webpack 36 | assetModuleFilename: '[name][ext]', 37 | clean: !isDev, 38 | }, 39 | resolve: { 40 | // IMPORTANT: help webpack find the wasm file 41 | alias: { 'MediaInfoModule.wasm': wasmFilePath }, 42 | extensions: ['.js', '.ts', '.jsx', '.tsx'], 43 | }, 44 | devtool: isDev ? 'cheap-module-source-map' : 'source-map', 45 | devServer: isDev ? { open: true } : undefined, 46 | plugins: [ 47 | new HtmlWebpackPlugin({ template: 'src/index.html' }), 48 | isDev && new ReactRefreshWebpackPlugin(), 49 | new ForkTsCheckerWebpackPlugin(), 50 | ].filter(Boolean), 51 | } 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🐞 Bug Report' 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: ['bug'] 6 | assignees: '' 7 | --- 8 | 9 | ### Checklist 10 | 11 | 14 | 15 | - [ ] I have searched the issue tracker for any duplicate issues and confirmed that this bug has not been reported before. 16 | - [ ] I have tested the issue with the upstream project [MediaInfo](https://github.com/MediaArea/MediaInfoLib) and can confirm that the problem only exists in mediainfo.js. 17 | - [ ] I have attached all necessary test files, if applicable, so that the issue can be easily reproduced by the developers. 18 | - [ ] I have added a reproduction repository or a code sandbox that clearly illustrates the issue. Providing a minimal example will greatly help the developers in understanding and resolving the problem. 19 | 20 | ### Bug Description 21 | 22 | 25 | 26 | ### Steps to Reproduce 27 | 28 | 31 | 32 | 1. ... 33 | 2. ... 34 | 3. ... 35 | 36 | ### Expected Behavior 37 | 38 | 41 | 42 | ### Actual Behavior 43 | 44 | 47 | 48 | ### Environment 49 | 50 | 53 | 54 | - mediainfo.js version: [e.g. 1.2.3] 55 | - Operating System: [e.g. Windows 10, macOS Big Sur] 56 | - Browser (if applicable): [e.g. Chrome 90, Firefox 87] 57 | - Node.js (if applicable): [e.g. 18.16.0] 58 | 59 | ### Additional Information 60 | 61 | 64 | -------------------------------------------------------------------------------- /gulp/generate-types/data/parseCsv.ts: -------------------------------------------------------------------------------- 1 | import { parse } from 'csv-parse/sync' 2 | 3 | import { downloadFile } from '../../utils' 4 | 5 | const url = (type: CsvType) => 6 | `https://raw.githubusercontent.com/MediaArea/MediaInfoLib/master/Source/Resource/Text/Stream/${type}.csv` 7 | 8 | const types = ['Audio', 'General', 'Image', 'Menu', 'Other', 'Text', 'Video'] as const 9 | 10 | function isCsvRecord(thing: unknown): thing is CsvRecord { 11 | return ( 12 | thing !== null && typeof thing === 'object' && typeof (thing as CsvRecord).name === 'string' 13 | ) 14 | } 15 | 16 | async function parseCsv(type: CsvType) { 17 | const csvData = await downloadFile(url(type)) 18 | 19 | const records = parse(csvData, { 20 | columns: ['name', '', '', '', '', '', 'description', '', 'group'], 21 | delimiter: ';', 22 | escape: false, 23 | quote: false, 24 | relaxColumnCountLess: true, 25 | skipEmptyLines: true, 26 | }) as object 27 | 28 | if (!Array.isArray(records)) { 29 | throw new TypeError('Expected array') 30 | } 31 | 32 | const descriptions: CsvRecords = {} 33 | 34 | for (const record of records) { 35 | if (!isCsvRecord(record)) { 36 | throw new Error(`Got malformed record: ${record}`) 37 | } 38 | descriptions[record.name.replaceAll('/', '_')] = record 39 | } 40 | 41 | return descriptions 42 | } 43 | 44 | async function parseCsvFiles() { 45 | const typeDescriptions: CsvData = { 46 | Audio: {}, 47 | General: {}, 48 | Image: {}, 49 | Menu: {}, 50 | Other: {}, 51 | Text: {}, 52 | Video: {}, 53 | } 54 | 55 | for (const type of types) { 56 | typeDescriptions[type] = await parseCsv(type) 57 | } 58 | 59 | return typeDescriptions 60 | } 61 | 62 | type CsvType = (typeof types)[number] 63 | 64 | interface CsvRecord { 65 | name: string 66 | description?: string 67 | group?: string 68 | } 69 | 70 | type CsvRecords = Record 71 | 72 | type CsvData = Record 73 | 74 | export type { CsvData, CsvRecords, CsvType } 75 | export default parseCsvFiles 76 | -------------------------------------------------------------------------------- /babel.config.cjs: -------------------------------------------------------------------------------- 1 | // Babel can't transpile to ES on-the-fly. So, we need to transform `import.meta.dirname` to old `__dirname`. 2 | function transformImportMetaDirname() { 3 | return { 4 | visitor: { 5 | MemberExpression(path) { 6 | if (path.node.object.type === 'MetaProperty' && path.node.property.name === 'dirname') { 7 | path.replaceWithSourceString('__dirname') 8 | } 9 | }, 10 | }, 11 | } 12 | } 13 | 14 | const babel = (api) => { 15 | api.cache(true) 16 | 17 | const browserTarget = '> 0.25%, not dead' 18 | const nodeTarget = { node: '18.0' } 19 | 20 | const buildMixin = { 21 | ignore: ['./__tests__', './**/*.d.ts'], 22 | sourceMaps: 'inline', 23 | } 24 | 25 | return { 26 | presets: [ 27 | ['@babel/preset-env', { modules: 'commonjs', targets: nodeTarget }], 28 | '@babel/preset-typescript', 29 | ], 30 | ignore: ['./**/*.d.ts'], 31 | sourceMaps: true, 32 | 33 | env: { 34 | // ESM build 35 | ESM: { 36 | presets: [['@babel/preset-env', { modules: false, targets: nodeTarget }]], 37 | ...buildMixin, 38 | }, 39 | 40 | // CommonJS build 41 | CJS: { 42 | presets: [['@babel/preset-env', { modules: 'commonjs', targets: nodeTarget }]], 43 | plugins: [ 44 | // Node.js CJS needs extensions in require statements 45 | ['babel-plugin-add-import-extension', { extension: 'cjs' }], 46 | ], 47 | ...buildMixin, 48 | }, 49 | 50 | // Bundled ESM 51 | ESM_ROLLUP: { 52 | presets: [['@babel/preset-env', { targets: browserTarget }]], 53 | ...buildMixin, 54 | }, 55 | 56 | // Gulp 57 | GULP: { 58 | presets: [['@babel/preset-env', { modules: false, targets: nodeTarget }]], 59 | plugins: [transformImportMetaDirname], 60 | sourceMaps: false, 61 | }, 62 | 63 | // Jest 64 | JEST: { 65 | presets: [['@babel/preset-env', { modules: 'commonjs', targets: nodeTarget }]], 66 | plugins: [transformImportMetaDirname], 67 | sourceMaps: false, 68 | }, 69 | }, 70 | } 71 | } 72 | 73 | module.exports = babel 74 | -------------------------------------------------------------------------------- /gulp/compile/optimizeWasm.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | 4 | import { BUILD_DIR, DIST_DIR, WASM_FILE } from '../constants' 5 | import { spawn } from '../utils' 6 | 7 | const DCE_CONFIG_FILE = path.join(BUILD_DIR, 'dceConfig.json') 8 | const DCE_WASM_PATH = path.join(BUILD_DIR, 'dce.wasm') 9 | 10 | async function extractExports() { 11 | const objdump = await spawn('wasm-objdump', ['-x', 'MediaInfoModule.wasm'], BUILD_DIR, true) 12 | const lines = objdump.split('\n') 13 | 14 | const reExport = /^ - \w+\[\d+] .+"(.+)"/ 15 | 16 | const exports: string[] = [] 17 | 18 | let foundExports = false 19 | while (true) { 20 | const line = lines.shift() 21 | if (line === undefined) { 22 | throw new Error('Failed to parse wasm-objdump output') 23 | } 24 | 25 | if (foundExports) { 26 | const match = reExport.exec(line) 27 | if (!match) { 28 | break 29 | } 30 | exports.push(match[1]) 31 | } else if (line.startsWith('Export[')) { 32 | foundExports = true 33 | } 34 | } 35 | 36 | return exports 37 | } 38 | 39 | interface ConfigItem { 40 | name: string 41 | root?: true 42 | reaches?: string[] 43 | export?: string 44 | } 45 | 46 | async function createDceConfig(exports: string[]) { 47 | const config: ConfigItem[] = [ 48 | { 49 | name: 'outside', 50 | root: true, 51 | reaches: exports.map((name) => `export-${name}`), 52 | }, 53 | ] 54 | for (const name of exports) { 55 | config.push({ 56 | name: `export-${name}`, 57 | export: name, 58 | }) 59 | } 60 | 61 | await fs.writeFile(DCE_CONFIG_FILE, JSON.stringify(config)) 62 | } 63 | 64 | async function optimizeWasm() { 65 | const exports = await extractExports() 66 | await createDceConfig(exports) 67 | await spawn( 68 | 'wasm-metadce', 69 | ['-all', '-f', DCE_CONFIG_FILE, '-o', DCE_WASM_PATH, WASM_FILE], 70 | BUILD_DIR 71 | ) 72 | await fs.mkdir(DIST_DIR, { recursive: true }) 73 | await spawn( 74 | 'wasm-opt', 75 | ['-c', '-Oz', '-o', path.join(DIST_DIR, WASM_FILE), DCE_WASM_PATH], 76 | BUILD_DIR 77 | ) 78 | } 79 | 80 | optimizeWasm.displayName = 'compile:optimize-wasm' 81 | optimizeWasm.description = 'Optimize WASM' 82 | 83 | export default optimizeWasm 84 | -------------------------------------------------------------------------------- /__tests__/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | 4 | import mediaInfoFactory, { isTrackType } from '..' 5 | import type { 6 | FormatType, 7 | MediaInfo, 8 | MediaInfoFactoryOptions, 9 | ReadChunkFunc, 10 | ResultMap, 11 | Track, 12 | } from '..' 13 | 14 | function fixturePath(name: string) { 15 | return path.resolve(import.meta.dirname, 'fixtures', name) 16 | } 17 | 18 | async function analyzeFile( 19 | filepath: string, 20 | opts?: MediaInfoFactoryOptions 21 | ) { 22 | let mi: MediaInfo | undefined 23 | let result: ResultMap[TFormat] 24 | 25 | try { 26 | mi = await mediaInfoFactory(opts) 27 | let fileHandle: fs.FileHandle | undefined 28 | 29 | const getSize = async () => { 30 | if (!fileHandle) { 31 | throw new Error('Should not happen') 32 | } 33 | const stat = await fileHandle.stat() 34 | return stat.size 35 | } 36 | const readChunk: ReadChunkFunc = async (size, offset) => { 37 | if (!fileHandle) { 38 | throw new Error('Should not happen') 39 | } 40 | const buffer = new Uint8Array(size) 41 | await fileHandle.read(buffer, 0, size, offset) 42 | return buffer 43 | } 44 | 45 | try { 46 | fileHandle = await fs.open(filepath, 'r') 47 | result = await mi.analyzeData(getSize, readChunk) 48 | } finally { 49 | if (fileHandle) { 50 | await fileHandle.close() 51 | } 52 | } 53 | } finally { 54 | if (mi) { 55 | mi.close() 56 | } 57 | } 58 | 59 | return result 60 | } 61 | 62 | function expectToBeDefined(arg: T): asserts arg is Exclude { 63 | expect(arg).toBeDefined() 64 | } 65 | 66 | function expectTrackType( 67 | thing: unknown, 68 | type: T 69 | ): asserts thing is Extract { 70 | expect(isTrackType(thing, type)).toBeTruthy() 71 | } 72 | 73 | function expectToBeError(error: unknown): asserts error is Error { 74 | expect( 75 | error !== null && 76 | typeof error === 'object' && 77 | Object.prototype.hasOwnProperty.call(error, 'message') 78 | ).toBeTruthy() 79 | } 80 | 81 | export { analyzeFile, expectToBeDefined, expectToBeError, expectTrackType, fixturePath } 82 | -------------------------------------------------------------------------------- /gulp/transpile/babel.ts: -------------------------------------------------------------------------------- 1 | import { copyFile } from 'node:fs/promises' 2 | import path from 'node:path' 3 | 4 | import gulp from 'gulp' 5 | import babel from 'gulp-babel' 6 | import rename from 'gulp-rename' 7 | import sourcemaps from 'gulp-sourcemaps' 8 | 9 | import { BUILD_DIR, DIST_DIR, SRC_DIR } from '../constants.ts' 10 | 11 | type Variant = 'cjs' | 'esm' 12 | 13 | function changeExtname(val: string) { 14 | return val.replace(/\.js$/, '.cjs') 15 | } 16 | 17 | function transpileBabel(variant: Variant) { 18 | const task = () => 19 | gulp 20 | .src([path.join(SRC_DIR, '**', '*.ts'), '!' + path.join(SRC_DIR, '**', '*.d.ts')]) 21 | .pipe(sourcemaps.init()) 22 | .pipe(babel({ envName: variant.toUpperCase() })) 23 | .pipe( 24 | rename((path) => { 25 | if (variant === 'esm') { 26 | return path 27 | } 28 | if (path.extname === '.js') { 29 | return { ...path, extname: '.cjs' } 30 | } 31 | return { ...path, basename: changeExtname(path.basename) } // .map 32 | }) 33 | ) 34 | .pipe( 35 | // gulp-sourcemaps patched to support .cjs extension 36 | sourcemaps.write('.', { 37 | sourceMappingURL: 38 | variant === 'cjs' ? undefined : (file) => `${changeExtname(file.relative)}.map`, 39 | mapFile: variant === 'cjs' ? changeExtname : undefined, 40 | sourceRoot: '../../src', 41 | }) 42 | ) 43 | .pipe(gulp.dest(path.join(DIST_DIR, variant))) 44 | task.displayName = `transpile:babel:${variant}` 45 | return task 46 | } 47 | 48 | function copyModuleLoader(variant: Variant) { 49 | const task = () => 50 | copyFile( 51 | path.join(BUILD_DIR, `MediaInfoModule.${variant}.js`), 52 | path.join(DIST_DIR, variant, `MediaInfoModule${variant === 'cjs' ? '.cjs' : '.js'}`) 53 | ) 54 | task.displayName = `transpile:babel:copy-loader:${variant}` 55 | return task 56 | } 57 | 58 | const esmTask = gulp.series([transpileBabel('esm'), copyModuleLoader('esm')]) 59 | const cjsTask = gulp.series([transpileBabel('cjs'), copyModuleLoader('cjs')]) 60 | 61 | const babelTask = gulp.parallel([esmTask, cjsTask]) 62 | babelTask.displayName = 'transpile:babel' 63 | babelTask.description = 'Transpile Node.js' 64 | 65 | export default babelTask 66 | -------------------------------------------------------------------------------- /gulp/generate-types/factories.ts: -------------------------------------------------------------------------------- 1 | import ts from 'typescript' 2 | 3 | export const exportModifier = ts.factory.createModifier(ts.SyntaxKind.ExportKeyword) 4 | export const readonlyModifier = ts.factory.createModifier(ts.SyntaxKind.ReadonlyKeyword) 5 | const questionToken = ts.factory.createToken(ts.SyntaxKind.QuestionToken) 6 | 7 | export function createProperty( 8 | name: string, 9 | typeName: string, 10 | opts: { array?: boolean; required?: boolean; readonly?: boolean } = {} 11 | ) { 12 | const defaultOpts = { array: false, required: false, readonly: true } 13 | const mergedOpts = { ...defaultOpts, ...opts } 14 | const refNode = ts.factory.createTypeReferenceNode(typeName) 15 | return ts.factory.createPropertySignature( 16 | mergedOpts.readonly ? [readonlyModifier] : undefined, 17 | name, 18 | mergedOpts.required ? undefined : questionToken, 19 | mergedOpts.array ? ts.factory.createArrayTypeNode(refNode) : refNode 20 | ) 21 | } 22 | 23 | export function createInterface( 24 | name: string, 25 | members: readonly ts.TypeElement[], 26 | extendsInterface?: string, 27 | modifiers: readonly ts.ModifierLike[] = [exportModifier] 28 | ) { 29 | return ts.factory.createInterfaceDeclaration( 30 | modifiers, 31 | name, 32 | undefined, 33 | extendsInterface 34 | ? [ 35 | ts.factory.createHeritageClause(ts.SyntaxKind.ExtendsKeyword, [ 36 | ts.factory.createExpressionWithTypeArguments( 37 | ts.factory.createIdentifier(extendsInterface), 38 | [] 39 | ), 40 | ]), 41 | ] 42 | : undefined, 43 | members 44 | ) 45 | } 46 | 47 | export function createArrayAsConst(name: string, elements: string[]) { 48 | return ts.factory.createVariableStatement( 49 | [exportModifier], 50 | ts.factory.createVariableDeclarationList( 51 | [ 52 | ts.factory.createVariableDeclaration( 53 | ts.factory.createIdentifier(name), 54 | undefined, 55 | undefined, 56 | ts.factory.createAsExpression( 57 | ts.factory.createArrayLiteralExpression( 58 | elements.map((e) => ts.factory.createStringLiteral(e)) 59 | ), 60 | ts.factory.createTypeReferenceNode('const') 61 | ) 62 | ), 63 | ], 64 | ts.NodeFlags.Const 65 | ) 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /gulp/constants.ts: -------------------------------------------------------------------------------- 1 | import { cpus } from 'node:os' 2 | import path from 'node:path' 3 | 4 | const PROJECT_DIR = path.resolve(import.meta.dirname, '..') 5 | const SRC_DIR = path.join(PROJECT_DIR, 'src') 6 | const DIST_DIR = path.join(PROJECT_DIR, 'dist') 7 | const BUILD_DIR = path.join(PROJECT_DIR, 'build') 8 | const VENDOR_DIR = path.join(BUILD_DIR, 'vendor') 9 | const WASM_FILE = 'MediaInfoModule.wasm' 10 | 11 | const TRACK_TYPES = ['General', 'Video', 'Audio', 'Text', 'Image', 'Menu', 'Other'] 12 | 13 | const WASM_INITIAL_MEMORY = 2 ** 25 // 32 MiB 14 | 15 | // Global variable name for UMD build 16 | const UMD_NAME = 'MediaInfo' 17 | 18 | const LIBMEDIAINFO_VERSION = '25.07' 19 | const LIBZEN_VERSION = '0.4.41' 20 | 21 | const CXXFLAGS = '-DEMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES=0 -fno-rtti -fno-exceptions' 22 | 23 | // switch off features to save some bytes 24 | const MediaInfoLib_CXXFLAGS = `-I ../../../Source -I ../../../../ZenLib/Source -s USE_ZLIB=1 \ 25 | -DMEDIAINFO_ADVANCED_YES \ 26 | -DMEDIAINFO_MINIMAL_YES \ 27 | -DMEDIAINFO_EXPORT_YES \ 28 | -DMEDIAINFO_SEEK_YES \ 29 | -DMEDIAINFO_READER_NO \ 30 | -DMEDIAINFO_REFERENCES_NO \ 31 | -DMEDIAINFO_GRAPH_NO \ 32 | -DMEDIAINFO_GRAPHVIZ_NO \ 33 | -DMEDIAINFO_ARCHIVE_NO \ 34 | -DMEDIAINFO_FIXITY_NO \ 35 | -DMEDIAINFO_CSV_NO \ 36 | -DMEDIAINFO_CUSTOM_NO \ 37 | -DMEDIAINFO_EBUCORE_NO \ 38 | -DMEDIAINFO_FIMS_NO \ 39 | -DMEDIAINFO_MPEG7_NO \ 40 | -DMEDIAINFO_PBCORE_NO \ 41 | -DMEDIAINFO_REVTMD_NO \ 42 | -DMEDIAINFO_NISO_NO \ 43 | -DMEDIAINFO_MINIMIZESIZE \ 44 | -DMEDIAINFO_TRACE_NO \ 45 | -DMEDIAINFO_FILTER_NO \ 46 | -DMEDIAINFO_DUPLICATE_NO \ 47 | -DMEDIAINFO_MACROBLOCKS_NO \ 48 | -DMEDIAINFO_NEXTPACKET_NO \ 49 | -DMEDIAINFO_EVENTS_NO \ 50 | -DMEDIAINFO_DEMUX_NO \ 51 | -DMEDIAINFO_IBI_NO \ 52 | -DMEDIAINFO_CONFORMANCE_YES \ 53 | -DMEDIAINFO_DIRECTORY_NO \ 54 | -DMEDIAINFO_LIBCURL_NO \ 55 | -DMEDIAINFO_LIBMMS_NO \ 56 | -DMEDIAINFO_READTHREAD_NO \ 57 | -DMEDIAINFO_MD5_NO \ 58 | -DMEDIAINFO_SHA1_NO \ 59 | -DMEDIAINFO_SHA2_NO \ 60 | -DMEDIAINFO_AES_NO \ 61 | -DMEDIAINFO_JNI_NO \ 62 | -DMEDIAINFO_TRACE_FFV1CONTENT_NO \ 63 | -DMEDIAINFO_COMPRESS \ 64 | -DMEDIAINFO_DECODE_NO \ 65 | -DMEDIAINFO_IBIUSAGE_NO` 66 | 67 | const CPU_CORES = cpus().length 68 | 69 | export { 70 | BUILD_DIR, 71 | CPU_CORES, 72 | CXXFLAGS, 73 | DIST_DIR, 74 | LIBMEDIAINFO_VERSION, 75 | LIBZEN_VERSION, 76 | MediaInfoLib_CXXFLAGS, 77 | PROJECT_DIR, 78 | SRC_DIR, 79 | TRACK_TYPES, 80 | UMD_NAME, 81 | VENDOR_DIR, 82 | WASM_FILE, 83 | WASM_INITIAL_MEMORY, 84 | } 85 | -------------------------------------------------------------------------------- /gulp/transpile/rollup.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | 4 | import babel from '@rollup/plugin-babel' 5 | import resolve from '@rollup/plugin-node-resolve' 6 | import terser from '@rollup/plugin-terser' 7 | import virtual from '@rollup/plugin-virtual' 8 | import gulp from 'gulp' 9 | import rollup from 'rollup' 10 | 11 | import { BUILD_DIR, DIST_DIR, SRC_DIR, UMD_NAME } from '../constants.ts' 12 | 13 | import type { OutputOptions } from 'rollup' 14 | 15 | interface Bundle { 16 | format: OutputOptions['format'] 17 | minify: boolean 18 | } 19 | 20 | let mediaInfoModuleContent: string 21 | 22 | async function loadMediaInfoModuleContent() { 23 | const modulePath = path.join(BUILD_DIR, 'MediaInfoModule.browser.js') 24 | mediaInfoModuleContent = await fs.readFile(modulePath, { encoding: 'utf8' }) 25 | } 26 | 27 | function makeBuildTask({ format, minify }: Bundle) { 28 | const task = async () => { 29 | const bundle = await rollup.rollup({ 30 | input: path.join(SRC_DIR, 'index.ts'), 31 | plugins: [ 32 | resolve({ extensions: ['.ts'] }), 33 | 34 | // The module loader generated by emscripten is different for each environment. 35 | // For the bundler we just inject the correct content. 36 | virtual({ 'src/MediaInfoModule.js': mediaInfoModuleContent }), 37 | 38 | babel({ 39 | babelHelpers: 'bundled', 40 | envName: 'ESM_ROLLUP', 41 | exclude: ['./node_modules/**', './src/cli.ts'], 42 | extensions: ['.ts'], 43 | include: ['./src/**'], 44 | }), 45 | ], 46 | }) 47 | 48 | const outputOptions = { 49 | file: path.join( 50 | DIST_DIR, 51 | format === 'esm' ? 'esm-bundle' : 'umd', 52 | `index${minify ? '.min' : ''}.js` 53 | ), 54 | format, 55 | name: format === 'umd' ? UMD_NAME : undefined, 56 | plugins: minify ? [terser()] : undefined, 57 | sourcemap: true, 58 | exports: 'named', 59 | } satisfies OutputOptions 60 | 61 | await bundle.write(outputOptions) 62 | } 63 | 64 | task.displayName = `transpile:rollup:${format}${minify ? ':min' : ''}` 65 | return task 66 | } 67 | 68 | const bundles: Bundle[] = [ 69 | { format: 'esm', minify: false }, 70 | { format: 'esm', minify: true }, 71 | { format: 'umd', minify: false }, 72 | { format: 'umd', minify: true }, 73 | ] 74 | 75 | const rollupTask = gulp.series([ 76 | loadMediaInfoModuleContent, 77 | gulp.parallel(bundles.map((build) => makeBuildTask(build))), 78 | ]) 79 | rollupTask.displayName = 'transpile:rollup' 80 | rollupTask.description = 'Transpile Browser bundles' 81 | 82 | export default rollupTask 83 | -------------------------------------------------------------------------------- /src/MediaInfoModule.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | class MediaInfoJs 6 | { 7 | MediaInfoLib::MediaInfo mi; 8 | 9 | public: 10 | MediaInfoJs(const MediaInfoLib::String &outputFormat, bool coverData, bool full) 11 | { 12 | mi.Option(__T("Output"), outputFormat); 13 | mi.Option(__T("File_IsSeekable"), __T("1")); 14 | if (coverData) 15 | { 16 | mi.Option(__T("Cover_Data"), __T("base64")); 17 | } 18 | if (full) 19 | { 20 | mi.Option(__T("Complete"), __T("1")); 21 | } 22 | } 23 | int open(const std::string &data, double fileSize) 24 | { 25 | return mi.Open((const ZenLib::int8u *)data.data(), data.size(), NULL, 0, (ZenLib::int64u)fileSize); 26 | } 27 | int open_buffer_init(double estimatedFileSize, double fileOffset) 28 | { 29 | return mi.Open_Buffer_Init((ZenLib::int64u)estimatedFileSize, (ZenLib::int64u)fileOffset); 30 | } 31 | int open_buffer_continue(const std::string &data, double size) 32 | { 33 | return mi.Open_Buffer_Continue((ZenLib::int8u *)data.data(), (ZenLib::int64u)size); 34 | } 35 | int open_buffer_finalize() 36 | { 37 | return mi.Open_Buffer_Finalize(); 38 | } 39 | int open_buffer_continue_goto_get() 40 | { 41 | return open_buffer_continue_goto_get_lower(); 42 | } 43 | // JS binding doesn't seem to support 64 bit int 44 | // see https://github.com/buzz/mediainfo.js/issues/11 45 | int open_buffer_continue_goto_get_lower() 46 | { 47 | return mi.Open_Buffer_Continue_GoTo_Get(); 48 | } 49 | int open_buffer_continue_goto_get_upper() 50 | { 51 | return mi.Open_Buffer_Continue_GoTo_Get() >> 32; 52 | } 53 | MediaInfoLib::String inform() 54 | { 55 | return mi.Inform(); 56 | } 57 | void close() 58 | { 59 | mi.Close(); 60 | } 61 | }; 62 | 63 | EMSCRIPTEN_BINDINGS(mediainfojs) 64 | { 65 | emscripten::class_("MediaInfo") 66 | .smart_ptr>("MediaInfo") 67 | .constructor() 68 | .function("open", &MediaInfoJs::open) 69 | .function("open_buffer_init", &MediaInfoJs::open_buffer_init) 70 | .function("open_buffer_continue", &MediaInfoJs::open_buffer_continue) 71 | .function("open_buffer_continue_goto_get", &MediaInfoJs::open_buffer_continue_goto_get) 72 | .function("open_buffer_continue_goto_get_lower", &MediaInfoJs::open_buffer_continue_goto_get_lower) 73 | .function("open_buffer_continue_goto_get_upper", &MediaInfoJs::open_buffer_continue_goto_get_upper) 74 | .function("open_buffer_finalize", &MediaInfoJs::open_buffer_finalize) 75 | .function("inform", &MediaInfoJs::inform) 76 | .function("close", &MediaInfoJs::close); 77 | } 78 | -------------------------------------------------------------------------------- /__tests__/jest/setup.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'node:crypto' 2 | import { createWriteStream, statSync } from 'node:fs' 3 | import { readFile } from 'node:fs/promises' 4 | import https from 'node:https' 5 | import path from 'node:path' 6 | 7 | const TEST_FILES = { 8 | 'AudioVideoInterleave.avi': { 9 | url: 'https://github.com/mathiasbynens/small/raw/master/AudioVideoInterleave.avi', 10 | md5: 'a51c3aff106210abcf32a9d4285628a6', 11 | }, 12 | 'Dead_Combo_-_01_-_Povo_Que_Cas_Descalo.mp3': { 13 | url: 'https://files.freemusicarchive.org/storage-freemusicarchive-org/music/Creative_Commons/Dead_Combo/CC_Affiliates_Mixtape_1/Dead_Combo_-_01_-_Povo_Que_Cas_Descalo.mp3', 14 | md5: 'b02fc030703403a13c9a6cef5922c6d1', 15 | }, 16 | // File has 0 bytes when downloaded... 17 | 'dwsample mp4 360p.mp4': { 18 | url: 'https://www.dwsamplefiles.com/?dl_id=348', 19 | md5: '6c104de9464c1b0c29f0510b5d520f53', 20 | }, 21 | 'many_tracks.mp4': { 22 | url: undefined, 23 | md5: '0e002574aad79365477ab8f904fef616', 24 | }, 25 | 'sample.mkv': { 26 | url: 'https://github.com/sbraz/pymediainfo/raw/master/tests/data/sample.mkv', 27 | md5: '130830537d5b0b79e78d68be16dde0fd', 28 | }, 29 | 'freeMXF-mxf1.mxf': { 30 | url: 'http://freemxf.org/samples/freeMXF-mxf1.mxf', 31 | md5: '25f21195085450603ba476d22ab85dae', 32 | }, 33 | } 34 | 35 | function downloadFile(url: string, filePath: string) { 36 | return new Promise((resolve, reject) => { 37 | https.get(url, (res) => { 38 | const code = res.statusCode ?? 0 39 | 40 | // handle redirects 41 | if (code > 300 && code < 400 && !!res.headers.location) { 42 | resolve(downloadFile(res.headers.location, filePath)) 43 | return 44 | } 45 | 46 | const fileStream = createWriteStream(filePath) 47 | .on('finish', () => { 48 | resolve() 49 | }) 50 | .on('error', (err) => { 51 | reject(err) 52 | }) 53 | 54 | res.pipe(fileStream) 55 | }) 56 | }) 57 | } 58 | 59 | async function downloadFixtures() { 60 | for (const [fileName, { url, md5 }] of Object.entries(TEST_FILES)) { 61 | const filePath = path.resolve(import.meta.dirname, '..', 'fixtures', fileName) 62 | 63 | // Check existing file 64 | try { 65 | statSync(filePath) 66 | } catch { 67 | // Download file 68 | if (url) { 69 | await downloadFile(url, filePath) 70 | } 71 | } 72 | 73 | // Check md5 hash 74 | const content = await readFile(filePath) 75 | const hash = crypto.createHash('md5').update(content).digest('hex') 76 | if (hash !== md5) { 77 | throw new Error(`File ${fileName} has md5 mismatch!`) 78 | } 79 | } 80 | } 81 | 82 | export default downloadFixtures 83 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { promises as fsPromises } from 'node:fs' 4 | 5 | import { hideBin } from 'yargs/helpers' 6 | import yargs from 'yargs/yargs' 7 | 8 | import { unknownToError } from './error.js' 9 | import { FORMAT_CHOICES } from './MediaInfo.js' 10 | import mediaInfoFactory from './mediaInfoFactory.js' 11 | import type { ReadChunkFunc } from './MediaInfo.js' 12 | import type MediaInfo from './MediaInfo.js' 13 | 14 | const analyze = async ({ coverData, file, format, full }: ReturnType) => { 15 | let fileHandle: fsPromises.FileHandle | undefined 16 | let fileSize: number 17 | let mediainfo: MediaInfo | undefined 18 | 19 | if (!file) { 20 | throw new TypeError('No file received!') 21 | } 22 | 23 | if (coverData && !['JSON', 'XML'].includes(format)) { 24 | throw new TypeError('For cover data you need to choose JSON or XML as output format!') 25 | } 26 | 27 | const readChunk: ReadChunkFunc = async (size, offset) => { 28 | if (fileHandle === undefined) { 29 | throw new Error('File unavailable') 30 | } 31 | const buffer = new Uint8Array(size) 32 | await fileHandle.read(buffer, 0, size, offset) 33 | return buffer 34 | } 35 | 36 | try { 37 | fileHandle = await fsPromises.open(file, 'r') 38 | const fileStat = await fileHandle.stat() 39 | fileSize = fileStat.size 40 | try { 41 | mediainfo = await mediaInfoFactory({ format, coverData, full }) 42 | } catch (error: unknown) { 43 | throw unknownToError(error) 44 | } 45 | console.log(await mediainfo.analyzeData(() => fileSize, readChunk)) 46 | } finally { 47 | if (fileHandle) { 48 | await fileHandle.close() 49 | } 50 | if (mediainfo) { 51 | mediainfo.close() 52 | } 53 | } 54 | } 55 | 56 | function parseArgs() { 57 | const yargsInstance = yargs(hideBin(process.argv)) 58 | return yargsInstance 59 | .wrap(yargsInstance.terminalWidth()) 60 | .option('format', { 61 | alias: 'f', 62 | default: 'text' as const, 63 | describe: 'Choose format', 64 | choices: FORMAT_CHOICES, 65 | }) 66 | .option('cover-data', { 67 | default: false, 68 | describe: 'Output cover data as base64', 69 | type: 'boolean', 70 | }) 71 | .option('full', { 72 | default: false, 73 | describe: 'Full information display (all internal tags)', 74 | type: 'boolean', 75 | }) 76 | .command('$0 ', 'Show information about media file') 77 | .positional('file', { describe: 'File to analyze', type: 'string' }) 78 | .help('h') 79 | .alias('h', 'help') 80 | .fail((message: string, error: Error, argv) => { 81 | if (message) { 82 | console.error(argv.help()) 83 | console.error(message) 84 | } 85 | console.error(error.message) 86 | process.exit(1) 87 | }) 88 | .parseSync() 89 | } 90 | 91 | try { 92 | await analyze(parseArgs()) 93 | } catch (error: unknown) { 94 | console.error(unknownToError(error).message) 95 | } 96 | -------------------------------------------------------------------------------- /gulp/utils.ts: -------------------------------------------------------------------------------- 1 | import { spawn as spawnChild } from 'node:child_process' 2 | import { createWriteStream } from 'node:fs' 3 | import { readFile, writeFile } from 'node:fs/promises' 4 | import https from 'node:https' 5 | 6 | import prettier from 'prettier' 7 | 8 | function downloadFile(url: string) { 9 | return new Promise((resolve, reject) => { 10 | https.get(url, (response) => { 11 | if (response.statusCode === 200) { 12 | let data = '' 13 | response.on('data', (chunk: string) => { 14 | data += chunk 15 | }) 16 | response 17 | .on('end', () => { 18 | resolve(data) 19 | }) 20 | .on('error', reject) 21 | } else { 22 | const msg = 23 | `Failed to download ${url}` + 24 | (response.statusCode === undefined ? '' : `, status=${response.statusCode}`) 25 | reject(new Error(msg)) 26 | } 27 | }) 28 | }) 29 | } 30 | 31 | function downloadFileToDir(url: string, destDir: string) { 32 | return new Promise((resolve, reject) => { 33 | const file = createWriteStream(destDir) 34 | 35 | https.get(url, function (response) { 36 | if (response.statusCode === 200) { 37 | response.pipe(file) 38 | file.on('finish', () => { 39 | file.close() 40 | resolve() 41 | }) 42 | file.on('error', (err) => { 43 | file.close() 44 | reject(err) 45 | }) 46 | } else { 47 | file.close() 48 | const msg = 49 | `Failed to download ${url}` + 50 | (response.statusCode === undefined ? '' : `, status=${response.statusCode}`) 51 | reject(new Error(msg)) 52 | } 53 | }) 54 | }) 55 | } 56 | 57 | async function format(filepath: string, destFilepath: string) { 58 | const text = await readFile(filepath, 'utf8') 59 | const options = await prettier.resolveConfig(filepath) 60 | if (options === null) { 61 | throw new Error('Could not find prettier config') 62 | } 63 | await writeFile(destFilepath, await prettier.format(text, { ...options, filepath })) 64 | } 65 | 66 | function spawn(cmd: string, args: string[], cwd: string, captureStdout = false) { 67 | return new Promise((resolve, reject) => { 68 | const proc = spawnChild(cmd, args, { cwd }) 69 | let output = '' 70 | 71 | proc.stdout.on('data', (data) => { 72 | if (Buffer.isBuffer(data)) { 73 | if (captureStdout) { 74 | output += data.toString() 75 | } else { 76 | process.stdout.write(data.toString()) 77 | } 78 | } 79 | }) 80 | 81 | proc.stderr.on('data', (data) => { 82 | if (Buffer.isBuffer(data)) { 83 | process.stderr.write(data.toString()) 84 | } 85 | }) 86 | 87 | proc.on('close', (code) => { 88 | if (code === 0) { 89 | resolve(output) 90 | } else { 91 | reject(new Error(`Program exited with status code ${code ?? 'null'}`)) 92 | } 93 | }) 94 | 95 | proc.stderr.on('error', (err) => { 96 | reject(err) 97 | }) 98 | }) 99 | } 100 | 101 | export { downloadFile, downloadFileToDir, format, spawn } 102 | -------------------------------------------------------------------------------- /gulp/compile/wasm.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/emscripten-core/emscripten/blob/main/src/settings.js 2 | 3 | import path from 'node:path' 4 | 5 | import gulp from 'gulp' 6 | 7 | import { BUILD_DIR, CXXFLAGS, MediaInfoLib_CXXFLAGS, WASM_INITIAL_MEMORY } from '../constants.ts' 8 | import { format, spawn } from '../utils.ts' 9 | import optimizeWasm from './optimizeWasm.ts' 10 | 11 | const moduleFilepath = path.join(BUILD_DIR, 'MediaInfoModule.js') 12 | 13 | function makeArgs(environment: 'web' | 'node', es6: boolean) { 14 | return [ 15 | ...CXXFLAGS.split(' '), 16 | ...MediaInfoLib_CXXFLAGS.split(' '), 17 | `-INITIAL_HEAP=${WASM_INITIAL_MEMORY}`, 18 | '-sALLOW_MEMORY_GROWTH=1', 19 | '-sMALLOC=emmalloc', 20 | '-sASSERTIONS=0', 21 | `-sENVIRONMENT=${environment}`, 22 | `-sEXPORT_ES6=${es6 ? '1' : '0'}`, 23 | '-sLEGACY_VM_SUPPORT=0', 24 | '-sMODULARIZE=1', 25 | '-sNO_FILESYSTEM=1', 26 | '-sEMBIND_STD_STRING_IS_UTF8=1', 27 | '-sINCOMING_MODULE_JS_API=locateFile', 28 | '--closure', 29 | '0', 30 | '-lembind', 31 | 'MediaInfoModule.o', 32 | 'vendor/MediaInfoLib/Project/GNU/Library/.libs/libmediainfo.a', 33 | 'vendor/ZenLib/Project/GNU/Library/.libs/libzen.a', 34 | '-o', 35 | moduleFilepath, 36 | ] 37 | } 38 | 39 | // MediaInfoModule.cpp -> MediaInfoModule.o 40 | function compileMediaInfoModule() { 41 | return spawn( 42 | 'emcc', 43 | [ 44 | ...CXXFLAGS.split(' '), 45 | ...MediaInfoLib_CXXFLAGS.split(' '), 46 | '-std=c++11', 47 | '-I', 48 | 'vendor/MediaInfoLib/Source', 49 | '-I', 50 | 'vendor/ZenLib/Source', 51 | '-c', 52 | '../src/MediaInfoModule.cpp', 53 | ], 54 | BUILD_DIR 55 | ) 56 | } 57 | 58 | compileMediaInfoModule.displayName = 'compile:mediainfomodule' 59 | compileMediaInfoModule.description = 'Compile MediaInfoModule' 60 | 61 | // MediaInfoModule.js (Node CJS) 62 | async function buildNodeCjs() { 63 | await spawn('emcc', makeArgs('node', false), BUILD_DIR) 64 | await format(moduleFilepath, path.join(BUILD_DIR, 'MediaInfoModule.cjs.js')) 65 | } 66 | 67 | buildNodeCjs.displayName = 'compile:node-cjs' 68 | buildNodeCjs.description = 'Build WASM (Node CJS)' 69 | 70 | // MediaInfoModule.js (Node ESM) 71 | async function buildNodeEsm() { 72 | await spawn('emcc', makeArgs('node', true), BUILD_DIR) 73 | await format(moduleFilepath, path.join(BUILD_DIR, 'MediaInfoModule.esm.js')) 74 | } 75 | 76 | buildNodeEsm.displayName = 'compile:node-esm' 77 | buildNodeEsm.description = 'Build WASM (Node ESM)' 78 | 79 | // MediaInfoModule.js (Browser) 80 | async function buildBrowser() { 81 | await spawn('emcc', makeArgs('web', true), BUILD_DIR) 82 | await format(moduleFilepath, path.join(BUILD_DIR, 'MediaInfoModule.browser.js')) 83 | } 84 | 85 | buildBrowser.displayName = 'compile:browser' 86 | buildBrowser.description = 'Build WASM (Browser)' 87 | 88 | const wasmTask = gulp.series([ 89 | compileMediaInfoModule, 90 | buildNodeCjs, 91 | buildNodeEsm, 92 | buildBrowser, 93 | optimizeWasm, 94 | ]) 95 | 96 | wasmTask.displayName = 'compile:wasm' 97 | wasmTask.description = 'Build WASM and loader code' 98 | 99 | export default wasmTask 100 | -------------------------------------------------------------------------------- /examples/angular/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "cli": { 5 | "packageManager": "pnpm" 6 | }, 7 | "newProjectRoot": "projects", 8 | "projects": { 9 | "angular": { 10 | "projectType": "application", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "inlineTemplate": true, 14 | "inlineStyle": true, 15 | "skipTests": true 16 | }, 17 | "@schematics/angular:class": { 18 | "skipTests": true 19 | }, 20 | "@schematics/angular:directive": { 21 | "skipTests": true 22 | }, 23 | "@schematics/angular:guard": { 24 | "skipTests": true 25 | }, 26 | "@schematics/angular:interceptor": { 27 | "skipTests": true 28 | }, 29 | "@schematics/angular:pipe": { 30 | "skipTests": true 31 | }, 32 | "@schematics/angular:resolver": { 33 | "skipTests": true 34 | }, 35 | "@schematics/angular:service": { 36 | "skipTests": true 37 | } 38 | }, 39 | "root": "", 40 | "sourceRoot": "src", 41 | "prefix": "app", 42 | "architect": { 43 | "build": { 44 | "builder": "@angular-devkit/build-angular:application", 45 | "options": { 46 | "outputPath": "dist/angular", 47 | "index": "src/index.html", 48 | "browser": "src/main.ts", 49 | "polyfills": [ 50 | "zone.js" 51 | ], 52 | "tsConfig": "tsconfig.app.json", 53 | "assets": [ 54 | { 55 | "input": "node_modules/mediainfo.js/dist", 56 | "glob": "MediaInfoModule.wasm", 57 | "output": "" 58 | } 59 | ], 60 | "styles": [], 61 | "scripts": [] 62 | }, 63 | "configurations": { 64 | "production": { 65 | "budgets": [ 66 | { 67 | "type": "initial", 68 | "maximumWarning": "500kB", 69 | "maximumError": "1MB" 70 | }, 71 | { 72 | "type": "anyComponentStyle", 73 | "maximumWarning": "2kB", 74 | "maximumError": "4kB" 75 | } 76 | ], 77 | "outputHashing": "all" 78 | }, 79 | "development": { 80 | "optimization": false, 81 | "extractLicenses": false, 82 | "sourceMap": true 83 | } 84 | }, 85 | "defaultConfiguration": "production" 86 | }, 87 | "serve": { 88 | "builder": "@angular-devkit/build-angular:dev-server", 89 | "configurations": { 90 | "production": { 91 | "buildTarget": "angular:build:production" 92 | }, 93 | "development": { 94 | "buildTarget": "angular:build:development" 95 | } 96 | }, 97 | "defaultConfiguration": "development" 98 | }, 99 | "extract-i18n": { 100 | "builder": "@angular-devkit/build-angular:extract-i18n" 101 | } 102 | } 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /__tests__/coverData.test.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'node:crypto' 2 | 3 | import { analyzeFile, expectToBeDefined, expectTrackType, fixturePath } from './utils.ts' 4 | import type { MediaInfoResult } from '..' 5 | 6 | const filePath = fixturePath('Dead_Combo_-_01_-_Povo_Que_Cas_Descalo.mp3') 7 | 8 | const testFields = (result: MediaInfoResult) => { 9 | const track = result.media?.track 10 | expectToBeDefined(track) 11 | 12 | expect(track).toHaveLength(3) 13 | 14 | const [track0, track1] = track 15 | 16 | expectTrackType(track0, 'General') 17 | expect(track0.Format).toBe('MPEG Audio') 18 | expect(track0.FileSize).toBe('6357777') 19 | expect(track0.Duration).toBeCloseTo(203.494) 20 | expect(track0.OverallBitRate_Mode).toBe('VBR') 21 | expect(track0.OverallBitRate).toBeNear(243_043, 2) 22 | expect(track0.StreamSize).toBe(175_116) 23 | expect(track0.Title).toBe('Povo Que Caís Descalço') 24 | expect(track0.Album).toBe('CC Affiliates Mixtape #1') 25 | expect(track0.Album_Performer).toBe('Creative Commons') 26 | expect(track0.Track).toBe('Povo Que Caís Descalço') 27 | expect(track0.Track_Position).toBe(1) 28 | expect(track0.Compilation).toBe('Yes') 29 | expect(track0.Performer).toBe('Dead Combo') 30 | expect(track0.Genre).toBe('International') 31 | expect(track0.Recorded_Date).toBe('2017-03-03 15:14:12 UTC') 32 | expect(track0.Encoded_Library).toBe('LAME3.99r') 33 | expect(track0.Copyright).toBe( 34 | 'Attribution-NonCommercial 3.0 International: http://creativecommons.org/licenses/by-nc/3.0/' 35 | ) 36 | expect(track0.Cover).toBe('Yes') 37 | expect(track0.Cover_Mime).toBe('image/jpeg') 38 | expect(track0.Comment).toBe( 39 | 'URL: http://freemusicarchive.org/music/Dead_Combo/Creative_Commons_The_2015_Unofficial_Mixtape/01_Povo_Que_Cais_Descalco / Comments: http://freemusicarchive.org/ / Curator: Creative Commons / Copyright: Attribution-NonCommercial 3.0 International: http://creativecommons.org/licenses/by-nc/3.0/' 40 | ) 41 | 42 | expectTrackType(track1, 'Audio') 43 | expect(track1.Format).toBe('MPEG Audio') 44 | expect(track1.Format_Version).toBe('1') 45 | expect(track1.Format_Profile).toBe('Layer 3') 46 | expect(track1.Format_Settings_Mode).toBe('Joint stereo') 47 | expect(track1.Duration).toBeCloseTo(203.493) 48 | expect(track1.BitRate_Mode).toBe('VBR') 49 | expect(track1.BitRate).toBeNear(243_043, 2) 50 | expect(track1.BitRate_Minimum).toBeCloseTo(32_000) 51 | expect(track1.Channels).toBe(2) 52 | expect(track1.SamplesPerFrame).toBeCloseTo(1152) 53 | expect(track1.SamplingRate).toBe(44_100) 54 | expect(track1.SamplingCount).toBe(8_974_080) 55 | expect(track1.FrameRate).toBeCloseTo(38.281) 56 | expect(track1.Compression_Mode).toBe('Lossy') 57 | expect(track1.StreamSize).toBe(6_182_244) 58 | expect(track1.Encoded_Library).toBe('LAME3.99r') 59 | expect(track1.Encoded_Library_Settings).toBe('-m j -V 0 -q 0 -lowpass 22.1 --vbr-mt -b 32') 60 | } 61 | 62 | describe('coverData: Dead_Combo_-_01_-_Povo_Que_Cas_Descalo.mp3', () => { 63 | it('should return cover data', async () => { 64 | const result = await analyzeFile(filePath, { coverData: true }) 65 | testFields(result) 66 | 67 | const track = result.media?.track 68 | expectToBeDefined(track) 69 | const [track0] = track 70 | expectTrackType(track0, 'General') 71 | const coverData = track0.Cover_Data ?? '' 72 | 73 | // Check cover data 74 | const coverDataDigest = crypto.createHash('md5').update(coverData).digest('hex') 75 | expect(coverDataDigest).toBe('10a34e34e74a39052dd26f16c76bc488') 76 | }) 77 | 78 | it('should not return cover data', async () => { 79 | const result = await analyzeFile(filePath, { coverData: false }) 80 | testFields(result) 81 | expect(result.media?.track[0]).not.toHaveProperty('Cover_Data') 82 | }) 83 | }) 84 | -------------------------------------------------------------------------------- /gulp/generate-types/data/parseXsd.ts: -------------------------------------------------------------------------------- 1 | import { assertIsNodeLike } from '@xmldom/is-dom-node' 2 | import { DOMParser } from '@xmldom/xmldom' 3 | import xpath from 'xpath' 4 | 5 | import { downloadFile } from '../../utils.ts' 6 | 7 | const URL = 'https://raw.githubusercontent.com/MediaArea/MediaAreaXml/master/mediainfo.xsd' 8 | const namespace = 'http://www.w3.org/2001/XMLSchema' 9 | 10 | async function parseXsd() { 11 | const xmlDocData = await downloadFile(URL) 12 | const parser = new DOMParser() 13 | const xmlDoc = parser.parseFromString(xmlDocData, 'text/xml') 14 | const select = xpath.useNamespaces({ xmlns: namespace }) 15 | 16 | assertIsNodeLike(xmlDoc) 17 | 18 | const elements = select('//xmlns:complexType[@name="trackType"]/xmlns:all/*', xmlDoc) 19 | 20 | if (!xpath.isArrayOfNodes(elements)) { 21 | throw new Error('No elements found!') 22 | } 23 | 24 | // Collect int/float types 25 | const intFields: string[] = [] 26 | const floatFields: string[] = [] 27 | const properties: Record = {} 28 | 29 | for (const element of elements) { 30 | if (!xpath.isElement(element)) { 31 | continue 32 | } 33 | 34 | let name = element.attributes.getNamedItem('name')?.value 35 | const minOccurs = element.attributes.getNamedItem('minOccurs')?.value 36 | const maxOccurs = element.attributes.getNamedItem('maxOccurs')?.value 37 | const xsdType = element.attributes.getNamedItem('type')?.value 38 | 39 | // fix typo is XSD 40 | if (name === 'Choregrapher') { 41 | name = 'Choreographer' 42 | } 43 | 44 | if ( 45 | name === undefined || 46 | minOccurs === undefined || 47 | maxOccurs === undefined || 48 | xsdType === undefined 49 | ) { 50 | throw new Error('Element missing attribute') 51 | } 52 | 53 | // all attributes should be optional 54 | if (minOccurs !== '0' || maxOccurs !== '1') { 55 | throw new Error(`minOccurs=${minOccurs} maxOccurs=${maxOccurs}`) 56 | } 57 | 58 | // extract type 59 | let type: string 60 | switch (xsdType) { 61 | case 'extraType': { 62 | type = 'Extra' 63 | break 64 | } 65 | case 'xsd:string': { 66 | type = 'string' 67 | break 68 | } 69 | case 'xsd:integer': { 70 | type = 'number' 71 | intFields.push(name) 72 | break 73 | } 74 | case 'xsd:float': { 75 | type = 'number' 76 | floatFields.push(name) 77 | break 78 | } 79 | default: { 80 | throw new Error(`Unknown type: ${xsdType}`) 81 | } 82 | } 83 | 84 | const property: XsdProperty = { type: type as PropertyType } 85 | 86 | // extract annotation if available 87 | let annotation: string | undefined 88 | const docEl = select('./xmlns:annotation/xmlns:documentation/text()', element) 89 | if (xpath.isArrayOfNodes(docEl) && xpath.isTextNode(docEl[0])) { 90 | annotation = docEl[0].nodeValue?.trim() 91 | if (!annotation) { 92 | throw new Error('Empty documentation element found.') 93 | } 94 | property.annotation = annotation 95 | } 96 | 97 | properties[name] = property 98 | } 99 | 100 | // Add fields missing from XSD 101 | properties.ISAN = { type: 'string' } 102 | properties.Active_Width_String = { type: 'string' } 103 | properties.Active_Height_String = { type: 'string' } 104 | properties.Active_DisplayAspectRatio_String = { type: 'string' } 105 | properties.HDR_Format_Compression = { type: 'string' } 106 | properties.Encoded_OperatingSystem_String = { type: 'string' } 107 | properties.Encoded_Hardware_String = { type: 'string' } 108 | 109 | return { properties, intFields, floatFields } 110 | } 111 | 112 | type PropertyType = 'string' | 'number' | 'Extra' 113 | 114 | interface XsdProperty { 115 | type: PropertyType 116 | annotation?: string 117 | } 118 | 119 | export type { XsdProperty } 120 | export default parseXsd 121 | -------------------------------------------------------------------------------- /src/mediaInfoFactory.ts: -------------------------------------------------------------------------------- 1 | import MediaInfo, { DEFAULT_OPTIONS, type FormatType } from './MediaInfo.js' 2 | import mediaInfoModuleFactory, { type MediaInfoModule } from './MediaInfoModule.js' 3 | 4 | interface MediaInfoFactoryOptions { 5 | /** Output cover data as base64 */ 6 | coverData?: boolean 7 | 8 | /** Chunk size used by `analyzeData` (in bytes) */ 9 | chunkSize?: number 10 | 11 | /** Result format (`object`, `JSON`, `XML`, `HTML` or `text`) */ 12 | format?: TFormat 13 | 14 | /** Full information display (all internal tags) */ 15 | full?: boolean 16 | 17 | /** 18 | * This method will be called before loading the WASM file. It should return the actual URL to 19 | * `MediaInfoModule.wasm`. 20 | * 21 | * @see https://emscripten.org/docs/api_reference/module.html#Module.locateFile 22 | */ 23 | locateFile?: (path: string, prefix: string) => string 24 | } 25 | 26 | const noopPrint = () => { 27 | // No-op 28 | } 29 | 30 | type FactoryCallback = (mediainfo: MediaInfo) => void 31 | type ErrorCallback = (error: unknown) => void 32 | 33 | function defaultLocateFile(path: string, prefix: string) { 34 | try { 35 | const url = new URL(prefix) 36 | if (url.pathname === '/') { 37 | return `${prefix}mediainfo.js/dist/${path}` 38 | } 39 | } catch { 40 | // empty 41 | } 42 | return `${prefix}../${path}` 43 | } 44 | 45 | // TODO pass through more emscripten module options? 46 | 47 | /** 48 | * Creates a {@link MediaInfo} instance with the specified options. 49 | * 50 | * @typeParam TFormat - The format type, defaults to `object`. 51 | * @param options - Configuration options for creating the {@link MediaInfo} instance. 52 | * @returns A promise that resolves to a {@link MediaInfo} instance when no callback is provided. 53 | */ 54 | function mediaInfoFactory( 55 | options?: MediaInfoFactoryOptions 56 | ): Promise> 57 | 58 | /** 59 | * Creates a {@link MediaInfo} instance with the specified options and executes the callback. 60 | * 61 | * @typeParam TFormat - The format type, defaults to `object`. 62 | * @param options - Configuration options for creating the {@link MediaInfo} instance. 63 | * @param callback - Function to call with the {@link MediaInfo} instance. 64 | * @param errCallback - Optional function to call on error. 65 | */ 66 | function mediaInfoFactory( 67 | options: MediaInfoFactoryOptions, 68 | callback: FactoryCallback, 69 | errCallback?: ErrorCallback 70 | ): void 71 | 72 | function mediaInfoFactory( 73 | options: MediaInfoFactoryOptions = {}, 74 | callback?: FactoryCallback, 75 | errCallback?: ErrorCallback 76 | ): Promise> | undefined { 77 | if (callback === undefined) { 78 | return new Promise((resolve, reject) => { 79 | mediaInfoFactory(options, resolve, reject) 80 | }) 81 | } 82 | 83 | const { locateFile, ...mergedOptions } = { 84 | ...DEFAULT_OPTIONS, 85 | ...options, 86 | format: (options.format ?? DEFAULT_OPTIONS.format) as TFormat, 87 | } 88 | 89 | const mediaInfoModuleFactoryOpts: Partial = { 90 | // Silence all print in module 91 | print: noopPrint, 92 | printErr: noopPrint, 93 | 94 | locateFile: locateFile ?? defaultLocateFile, 95 | onAbort: (err: Error) => { 96 | if (errCallback) { 97 | errCallback(err) 98 | } 99 | }, 100 | } 101 | 102 | // Fetch and load WASM module 103 | mediaInfoModuleFactory(mediaInfoModuleFactoryOpts) 104 | .then((wasmModule) => { 105 | callback(new MediaInfo(wasmModule, mergedOptions)) 106 | }) 107 | .catch((error: unknown) => { 108 | if (errCallback) { 109 | errCallback(error) 110 | } 111 | }) 112 | } 113 | 114 | export type { MediaInfoFactoryOptions } 115 | export default mediaInfoFactory 116 | -------------------------------------------------------------------------------- /__tests__/options.test.ts: -------------------------------------------------------------------------------- 1 | import { assertIsNodeLike } from '@xmldom/is-dom-node' 2 | import { DOMParser } from '@xmldom/xmldom' 3 | import xpath from 'xpath' 4 | 5 | import mediaInfoFactory from '..' 6 | import { expectToBeDefined, expectTrackType } from './utils' 7 | import type { FormatType, MediaInfo, ResultMap } from '..' 8 | 9 | function analyzeFakeData(mi: MediaInfo) { 10 | return mi.analyzeData( 11 | () => 20, 12 | () => new Uint8Array(10) 13 | ) 14 | } 15 | 16 | it('should use default options', async () => { 17 | expect.assertions(4) 18 | let mi: MediaInfo | undefined 19 | 20 | try { 21 | mi = await mediaInfoFactory() 22 | expect(mi.options.coverData).toBe(false) 23 | expect(mi.options.chunkSize).toBe(256 * 1024) 24 | expect(mi.options.full).toBe(false) 25 | expect(await analyzeFakeData(mi)).toBeInstanceOf(Object) 26 | } finally { 27 | if (mi) { 28 | mi.close() 29 | } 30 | } 31 | }) 32 | 33 | it('should accepts options', async () => { 34 | expect.assertions(6) 35 | let mi: MediaInfo | undefined 36 | 37 | try { 38 | mi = await mediaInfoFactory({ 39 | chunkSize: 16 * 1024, 40 | coverData: true, 41 | format: 'object', 42 | full: true, 43 | }) 44 | expect(mi.options.chunkSize).toBe(16 * 1024) 45 | expect(mi.options.coverData).toBe(true) 46 | expect(mi.options.full).toBe(true) 47 | const result = await analyzeFakeData(mi) 48 | expectToBeDefined(result.media) 49 | const [track0] = result.media.track 50 | expectTrackType(track0, 'General') 51 | expect(track0.FileSize).toBe('20') 52 | } finally { 53 | if (mi) { 54 | mi.close() 55 | } 56 | } 57 | }) 58 | 59 | it('should return JSON string', async () => { 60 | expect.assertions(5) 61 | let mi: MediaInfo<'JSON'> | undefined 62 | 63 | try { 64 | mi = await mediaInfoFactory({ format: 'JSON' }) 65 | const result = await analyzeFakeData(mi) 66 | expect(result).toEqual(expect.any(String)) 67 | let fileSize: string | undefined 68 | expect(() => { 69 | const resultObj = JSON.parse(result) as ResultMap['object'] 70 | expectToBeDefined(resultObj.media) 71 | const [track0] = resultObj.media.track 72 | expectTrackType(track0, 'General') 73 | fileSize = track0.FileSize 74 | }).not.toThrow() 75 | expect(fileSize).toBe('20') 76 | } finally { 77 | if (mi) { 78 | mi.close() 79 | } 80 | } 81 | }) 82 | 83 | it('should return HTML string', async () => { 84 | expect.assertions(4) 85 | let mi: MediaInfo<'HTML'> | undefined 86 | 87 | try { 88 | mi = await mediaInfoFactory({ format: 'HTML' }) 89 | const result = await analyzeFakeData(mi) 90 | expect(result).toEqual(expect.any(String)) 91 | expect(result).toMatch(/') 94 | } finally { 95 | if (mi) { 96 | mi.close() 97 | } 98 | } 99 | }) 100 | 101 | it('should return formatted text string', async () => { 102 | expect.assertions(3) 103 | let mi: MediaInfo<'text'> | undefined 104 | 105 | try { 106 | mi = await mediaInfoFactory({ format: 'text' }) 107 | const result = await analyzeFakeData(mi) 108 | expect(result).toEqual(expect.any(String)) 109 | expect(result).toMatch('File size') 110 | expect(result).toMatch('20.0 Bytes') 111 | } finally { 112 | if (mi) { 113 | mi.close() 114 | } 115 | } 116 | }) 117 | 118 | it('should return XML string', async () => { 119 | expect.assertions(2) 120 | let mi: MediaInfo<'XML'> | undefined 121 | 122 | try { 123 | mi = await mediaInfoFactory({ format: 'XML' }) 124 | const result = await analyzeFakeData(mi) 125 | expect(result).toEqual(expect.any(String)) 126 | 127 | const parser = new DOMParser() 128 | const doc = parser.parseFromString(result, 'text/xml') 129 | assertIsNodeLike(doc) 130 | const select = xpath.useNamespaces({ mi: 'https://mediaarea.net/mediainfo' }) 131 | 132 | const elems = select('//mi:media/mi:track/mi:FileSize/text()', doc) 133 | 134 | if (!xpath.isArrayOfNodes(elems) || !xpath.isTextNode(elems[0])) { 135 | throw new Error('FileSize not found') 136 | } 137 | 138 | expect(elems[0].nodeValue?.trim()).toBe('20') 139 | } finally { 140 | if (mi) { 141 | mi.close() 142 | } 143 | } 144 | }) 145 | -------------------------------------------------------------------------------- /gulp/generate-types/data/getFields.ts: -------------------------------------------------------------------------------- 1 | import parseCsv from './parseCsv.ts' 2 | import parseXsd from './parseXsd.ts' 3 | import type { CsvData, CsvRecords, CsvType } from './parseCsv.ts' 4 | import type { XsdProperty } from './parseXsd.ts' 5 | 6 | interface TrackField { 7 | description?: string 8 | group?: string 9 | type: string 10 | } 11 | 12 | type TrackFields = Record 13 | 14 | type TrackTypes = Record<'Base' | CsvType, TrackFields> 15 | 16 | type GetXsdPropery = (name: string) => XsdProperty 17 | 18 | function normalizeName(name: string) { 19 | let normalizedName = name.replace('*', '_') 20 | for (const char of ['(', ')']) { 21 | normalizedName = normalizedName.replace(char, '') 22 | } 23 | return normalizedName 24 | } 25 | 26 | function findCommonFields(descriptions: CsvData, getXsdType: GetXsdPropery): TrackFields { 27 | const commonFields: TrackFields = {} 28 | 29 | const { General: general, ...others } = descriptions 30 | 31 | // Find fields present in every track type 32 | for (const csvName of Object.keys(general)) { 33 | // Ignore `Title` as it has different descriptions that are worth keeping 34 | if (csvName === 'Title') { 35 | continue 36 | } 37 | 38 | if (Object.values(others).every((descr) => Object.keys(descr).includes(csvName))) { 39 | const normalizedName = normalizeName(csvName) 40 | 41 | // Add to common 42 | commonFields[normalizedName] = { 43 | description: general[csvName].description, // ok: all common properties have descriptions 44 | group: general[csvName].group, 45 | type: getXsdType(normalizedName).type, 46 | } 47 | } 48 | } 49 | 50 | return commonFields 51 | } 52 | 53 | function makeTrackFields( 54 | csvRecords: CsvRecords, 55 | commonFieldsNames: string[], 56 | getXsdType: GetXsdPropery 57 | ): TrackFields { 58 | const fields: TrackFields = {} 59 | 60 | for (const [csvName, record] of Object.entries(csvRecords)) { 61 | const normalizedName = normalizeName(csvName) 62 | 63 | // Skip fields present in `BaseTrack` 64 | if (commonFieldsNames.includes(normalizedName)) { 65 | continue 66 | } 67 | 68 | let xsdProperty: XsdProperty 69 | try { 70 | xsdProperty = getXsdType(normalizedName) 71 | } catch (error) { 72 | if (error instanceof Error && error.message === "Property 'Type_String' not found in XSD") { 73 | // 'Type_String' is missing in 74 | // https://github.com/MediaArea/MediaAreaXml/blob/master/mediainfo.xsd 75 | xsdProperty = { type: 'string' } 76 | } else { 77 | throw error 78 | } 79 | } 80 | 81 | const field: TrackField = { 82 | type: xsdProperty.type, 83 | group: record.group, 84 | } 85 | 86 | // try description from CSV, or XSD 87 | if (record.description) { 88 | field.description = record.description 89 | } else if (xsdProperty.annotation) { 90 | field.description = xsdProperty.annotation 91 | } 92 | 93 | // Skip deprecated 94 | if (field.description?.toLowerCase().includes('deprecated')) { 95 | continue 96 | } 97 | 98 | fields[normalizedName] = field 99 | } 100 | 101 | return fields 102 | } 103 | 104 | async function getFields(): Promise<[TrackTypes, string[], string[]]> { 105 | // Parse XSD 106 | const { properties: xsdProperties, intFields, floatFields } = await parseXsd() 107 | 108 | // Parse CSV 109 | const csvData = await parseCsv() 110 | 111 | const getXsdType = (fieldName: string): XsdProperty => { 112 | if (!Object.keys(xsdProperties).includes(fieldName)) { 113 | throw new Error(`Property '${fieldName}' not found in XSD`) 114 | } 115 | return xsdProperties[fieldName] 116 | } 117 | 118 | // Find common fields to put into `BaseTrack` 119 | const commonFields = findCommonFields(csvData, getXsdType) 120 | const commonFieldNames = Object.keys(commonFields) 121 | 122 | return [ 123 | { 124 | Base: commonFields, 125 | Audio: makeTrackFields(csvData.Audio, commonFieldNames, getXsdType), 126 | General: makeTrackFields(csvData.General, commonFieldNames, getXsdType), 127 | Image: makeTrackFields(csvData.Image, commonFieldNames, getXsdType), 128 | Menu: makeTrackFields(csvData.Menu, commonFieldNames, getXsdType), 129 | Other: makeTrackFields(csvData.Other, commonFieldNames, getXsdType), 130 | Text: makeTrackFields(csvData.Text, commonFieldNames, getXsdType), 131 | Video: makeTrackFields(csvData.Video, commonFieldNames, getXsdType), 132 | }, 133 | intFields, 134 | floatFields, 135 | ] 136 | } 137 | 138 | export type { TrackFields } 139 | export default getFields 140 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import eslintJs from '@eslint/js' 2 | import eslintPluginImport from 'eslint-plugin-import' 3 | import eslintPluginJest from 'eslint-plugin-jest' 4 | import eslintPluginPrettier from 'eslint-plugin-prettier' 5 | import eslintPluginSimpleImportSort from 'eslint-plugin-simple-import-sort' 6 | import eslintPluginUnicorn from 'eslint-plugin-unicorn' 7 | import globals from 'globals' 8 | import tsEslint from 'typescript-eslint' 9 | 10 | const groupWithTypes = (/** @type {string} */ re) => [re, `${re}.*\\u0000$`] 11 | 12 | export default tsEslint.config( 13 | // plugins 14 | 15 | { 16 | plugins: { 17 | import: eslintPluginImport, 18 | prettier: eslintPluginPrettier, 19 | 'simple-import-sort': eslintPluginSimpleImportSort, 20 | }, 21 | }, 22 | 23 | // extends 24 | 25 | eslintJs.configs.recommended, 26 | ...tsEslint.configs.strictTypeChecked, 27 | ...tsEslint.configs.stylisticTypeChecked, 28 | eslintPluginUnicorn.configs['flat/recommended'], 29 | 30 | // base config 31 | 32 | { 33 | languageOptions: { 34 | globals: globals.es2021, 35 | parserOptions: { 36 | project: true, 37 | tsconfigRootDir: import.meta.dirname, 38 | }, 39 | }, 40 | rules: { 41 | ...eslintPluginImport.configs.typescript.rules, 42 | ...eslintPluginPrettier.configs.recommended.rules, 43 | 44 | // Enforce all block statements to be wrapped in curly braces 45 | curly: 'error', 46 | 47 | // disable as we're using @typescript-eslint/no-restricted-imports 48 | 'no-restricted-imports': 'off', 49 | 50 | // TypeScript provides the same checks 51 | // https://typescript-eslint.io/linting/troubleshooting/performance-troubleshooting#eslint-plugin-import 52 | 'import/named': 'off', 53 | 'import/namespace': 'off', 54 | 'import/default': 'off', 55 | 'import/no-named-as-default-member': 'off', 56 | // Import order setup 57 | 'import/first': 'error', 58 | 'import/no-duplicates': 'error', 59 | 'import/extensions': ['error', 'ignorePackages'], 60 | 61 | 'prettier/prettier': 'error', 62 | 63 | 'simple-import-sort/imports': [ 64 | 'error', 65 | { 66 | // custom groups with type imports last in each group 67 | // https://github.com/lydell/eslint-plugin-simple-import-sort#custom-grouping 68 | groups: [ 69 | [String.raw`^\u0000`], // side-effects 70 | groupWithTypes('^node:'), // node modules 71 | [String.raw`(?=18.0.0" 38 | }, 39 | "bin": { 40 | "mediainfo.js": "./dist/esm/cli.js" 41 | }, 42 | "type": "module", 43 | "main": "./dist/cjs/index.cjs", 44 | "module": "./dist/esm/index.js", 45 | "browser": "./dist/esm-bundle/index.min.js", 46 | "types": "./dist/index.d.ts", 47 | "unpkg": "./dist/umd/index.min.js", 48 | "jsdelivr": "./dist/umd/index.min.js", 49 | "exports": { 50 | ".": { 51 | "types": "./dist/index.d.ts", 52 | "module": "./dist/esm-bundle/index.js", 53 | "import": "./dist/esm/index.js", 54 | "require": "./dist/cjs/index.cjs", 55 | "default": "./dist/esm/index.js" 56 | }, 57 | "./MediaInfoModule.wasm": "./dist/MediaInfoModule.wasm", 58 | "./package.json": "./package.json" 59 | }, 60 | "commitlint": { 61 | "extends": [ 62 | "@commitlint/config-conventional" 63 | ] 64 | }, 65 | "commit-and-tag-version": { 66 | "types": [ 67 | { 68 | "type": "feat", 69 | "section": "Features" 70 | }, 71 | { 72 | "type": "fix", 73 | "section": "Bug Fixes" 74 | }, 75 | { 76 | "type": "build", 77 | "scope": "deps", 78 | "section": "Upgrades" 79 | }, 80 | { 81 | "type": "chore", 82 | "hidden": true 83 | }, 84 | { 85 | "type": "docs", 86 | "hidden": true 87 | }, 88 | { 89 | "type": "style", 90 | "hidden": true 91 | }, 92 | { 93 | "type": "refactor", 94 | "hidden": true 95 | }, 96 | { 97 | "type": "perf", 98 | "hidden": true 99 | }, 100 | { 101 | "type": "test", 102 | "hidden": true 103 | } 104 | ] 105 | }, 106 | "dependencies": { 107 | "yargs": "^18.0.0" 108 | }, 109 | "devDependencies": { 110 | "@babel/cli": "^7.28.3", 111 | "@babel/core": "^7.28.3", 112 | "@babel/preset-env": "^7.28.3", 113 | "@babel/preset-typescript": "^7.27.1", 114 | "@babel/register": "^7.28.3", 115 | "@commitlint/cli": "^19.8.1", 116 | "@commitlint/config-conventional": "^19.8.1", 117 | "@eslint/js": "^9.34.0", 118 | "@jest/globals": "^30.1.1", 119 | "@rollup/plugin-babel": "^6.0.4", 120 | "@rollup/plugin-node-resolve": "^16.0.1", 121 | "@rollup/plugin-terser": "^0.4.4", 122 | "@rollup/plugin-virtual": "^3.0.2", 123 | "@types/decompress": "^4.2.7", 124 | "@types/emscripten": "^1.40.1", 125 | "@types/gulp": "^4.0.17", 126 | "@types/gulp-rename": "^2.0.6", 127 | "@types/gulp-sourcemaps": "^0.0.38", 128 | "@types/jest": "^30.0.0", 129 | "@types/node": "^24.3.0", 130 | "@types/vinyl-buffer": "^1.0.3", 131 | "@types/vinyl-source-stream": "^0.0.34", 132 | "@types/yargs": "^17.0.33", 133 | "@xmldom/is-dom-node": "^1.0.1", 134 | "@xmldom/xmldom": "^0.9.8", 135 | "babel-jest": "^30.1.1", 136 | "babel-plugin-add-import-extension": "^1.6.0", 137 | "commit-and-tag-version": "^12.6.0", 138 | "csv-parse": "^6.1.0", 139 | "decompress": "^4.2.1", 140 | "eslint": "^9.34.0", 141 | "eslint-plugin-import": "^2.32.0", 142 | "eslint-plugin-jest": "^29.0.1", 143 | "eslint-plugin-prettier": "^5.5.4", 144 | "eslint-plugin-simple-import-sort": "^12.1.1", 145 | "eslint-plugin-unicorn": "^60.0.0", 146 | "globals": "^16.3.0", 147 | "gulp": "^5.0.1", 148 | "gulp-babel": "^8.0.0", 149 | "gulp-cli": "^3.1.0", 150 | "gulp-rename": "^2.1.0", 151 | "gulp-sourcemaps": "^3.0.0", 152 | "husky": "^9.1.7", 153 | "jest": "^30.1.1", 154 | "jest-environment-node": "^30.1.1", 155 | "jest-matcher-utils": "^30.1.1", 156 | "prettier": "^3.6.2", 157 | "rimraf": "^6.0.1", 158 | "rollup": "^4.49.0", 159 | "typescript": "^5.9.2", 160 | "typescript-eslint": "^8.41.0", 161 | "xpath": "^0.0.34" 162 | }, 163 | "pnpm": { 164 | "patchedDependencies": { 165 | "gulp-sourcemaps@3.0.0": "patches/gulp-sourcemaps@3.0.0.patch" 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /gulp/generate-types/generate.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs/promises' 2 | import path from 'node:path' 3 | 4 | import ts from 'typescript' 5 | 6 | import { BUILD_DIR, SRC_DIR, TRACK_TYPES } from '../constants.ts' 7 | import { format } from '../utils.ts' 8 | import getFields, { type TrackFields } from './data/getFields.ts' 9 | import { 10 | createArrayAsConst, 11 | createInterface, 12 | createProperty, 13 | exportModifier, 14 | readonlyModifier, 15 | } from './factories.ts' 16 | 17 | const TOP_COMMENT = '// DO NOT EDIT! File generated using `generate-types` script.' 18 | const FILENAME = 'MediaInfoResult.ts' 19 | 20 | const outFilename = path.join(SRC_DIR, FILENAME) 21 | 22 | const creationInfo = createInterface('CreationInfo', [ 23 | createProperty('version', 'string', { required: true }), 24 | createProperty('url', 'string'), 25 | createProperty('build_date', 'string'), 26 | createProperty('build_time', 'string'), 27 | createProperty('compiler_ident', 'string'), 28 | ]) 29 | 30 | const extra = ts.factory.createTypeAliasDeclaration( 31 | [exportModifier], 32 | ts.factory.createIdentifier('Extra'), 33 | undefined, 34 | ts.factory.createTypeReferenceNode(ts.factory.createIdentifier('Record'), [ 35 | ts.factory.createKeywordTypeNode(ts.SyntaxKind.StringKeyword), 36 | ts.factory.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword), 37 | ]) 38 | ) 39 | 40 | function wrapWithComment(node: ts.PropertySignature, comment: string) { 41 | return ts.addSyntheticLeadingComment( 42 | node, 43 | ts.SyntaxKind.MultiLineCommentTrivia, 44 | `* ${comment} `, 45 | true 46 | ) 47 | } 48 | 49 | function makeBaseTrack(fields: TrackFields) { 50 | return createInterface('BaseTrack', [ 51 | wrapWithComment( 52 | ts.factory.createPropertySignature( 53 | [readonlyModifier], 54 | "'@type'", 55 | undefined, 56 | ts.factory.createUnionTypeNode( 57 | TRACK_TYPES.map((t) => 58 | ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(t)) 59 | ) 60 | ) 61 | ), 62 | 'Documents the type of encoded media with the track, ie: ' + 63 | 'General, Video, Audio, Text, Image, etc.' 64 | ), 65 | wrapWithComment( 66 | createProperty("'@typeorder'", 'string'), 67 | 'If there is more than one track of the same type (i.e. four audio tracks) this ' + 68 | 'attribute will number them according to storage order within the bitstream.' 69 | ), 70 | wrapWithComment( 71 | createProperty('extra', 'Extra'), 72 | 'Holds (untyped) extra information if available' 73 | ), 74 | ...makeTrackMembers(fields), 75 | ]) 76 | } 77 | 78 | const track = ts.factory.createTypeAliasDeclaration( 79 | [exportModifier], 80 | ts.factory.createIdentifier('Track'), 81 | undefined, 82 | ts.factory.createUnionTypeNode( 83 | TRACK_TYPES.map((type) => ts.factory.createTypeReferenceNode(`${type}Track`)) 84 | ) 85 | ) 86 | 87 | const media = createInterface('Media', [ 88 | createProperty("'@ref'", 'string', { required: true }), 89 | createProperty('track', 'Track', { array: true, required: true }), 90 | ]) 91 | 92 | const mediaInfoResult = createInterface('MediaInfoResult', [ 93 | createProperty('creatingApplication', 'CreationInfo'), 94 | createProperty('creatingLibrary', 'CreationInfo'), 95 | createProperty('media', 'Media'), 96 | ]) 97 | 98 | function makeTrackMembers(fields: TrackFields) { 99 | const members: ts.TypeElement[] = [] 100 | 101 | for (const [propertyName, field] of Object.entries(fields)) { 102 | const tsPropertyName = propertyName.includes('-') ? `'${propertyName}'` : propertyName 103 | const property = createProperty(tsPropertyName, field.type) 104 | 105 | // Add `@internal` tag 106 | if (field.description?.includes('This is mostly for internal use')) { 107 | field.description = field.description.replace('This is mostly for internal use', '@internal') 108 | } 109 | 110 | // Add `@group` tag 111 | if (field.group) { 112 | field.description = `${field.description} @group ${field.group}` 113 | } 114 | 115 | members.push(field.description ? wrapWithComment(property, field.description) : property) 116 | } 117 | 118 | return members 119 | } 120 | 121 | function makeTrackInterfaces(trackTypes: Record) { 122 | return Object.entries(trackTypes).map(([trackType, fields]) => { 123 | const members = makeTrackMembers(fields) 124 | 125 | const typeProperty = ts.factory.createPropertySignature( 126 | [readonlyModifier], 127 | "'@type'", 128 | undefined, 129 | ts.factory.createLiteralTypeNode(ts.factory.createStringLiteral(trackType)) 130 | ) 131 | 132 | return createInterface(`${trackType}Track`, [typeProperty, ...members], 'BaseTrack') 133 | }) 134 | } 135 | 136 | async function generate() { 137 | const [trackTypes, intFields, floatFields] = await getFields() 138 | 139 | const { Base: baseTrackFields, ...otherTrackTypes } = trackTypes 140 | 141 | // Generate source code 142 | const allNodes = [ 143 | createArrayAsConst('INT_FIELDS', intFields), 144 | createArrayAsConst('FLOAT_FIELDS', floatFields), 145 | creationInfo, 146 | extra, 147 | makeBaseTrack(baseTrackFields), 148 | ...makeTrackInterfaces(otherTrackTypes), 149 | track, 150 | media, 151 | mediaInfoResult, 152 | ] 153 | const file = ts.createSourceFile('DUMMY.ts', '', ts.ScriptTarget.ESNext, false, ts.ScriptKind.TS) 154 | const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }) 155 | const tsSrc = [ 156 | TOP_COMMENT, 157 | ...allNodes.map((node) => printer.printNode(ts.EmitHint.Unspecified, node, file)), 158 | ].join('\n\n') 159 | 160 | // Save generated source 161 | const buildOutFilename = path.join(BUILD_DIR, FILENAME) 162 | await fs.writeFile(buildOutFilename, tsSrc) 163 | 164 | // Format sources 165 | await format(buildOutFilename, outFilename) 166 | } 167 | 168 | generate.displayName = 'generate-types' 169 | generate.description = 'Generate MediaInfo result types from XSD' 170 | 171 | export default generate 172 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | :::note 6 | Changes preceding version 0.2.0 are not included in the changelog. 7 | ::: 8 | 9 | ## [0.3.6](https://github.com/buzz/mediainfo.js/compare/v0.3.5...v0.3.6) (2025-08-31) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * prevent multiple simultaneous analysis runs ([3b0c2cb](https://github.com/buzz/mediainfo.js/commit/3b0c2cb2e073c83902a748fd97c7cb01f424f03b)), closes [#173](https://github.com/buzz/mediainfo.js/issues/173) 15 | * reset MediaInfo module instance before analyze ([2a9e587](https://github.com/buzz/mediainfo.js/commit/2a9e587a99824a9870373fb0e2c76aaf84589256)), closes [#177](https://github.com/buzz/mediainfo.js/issues/177) 16 | * reset MediaInfoModule to its initial state ([3444be5](https://github.com/buzz/mediainfo.js/commit/3444be577d98df8432f03cf44061d41af8338fa9)) 17 | 18 | 19 | ### Upgrades 20 | 21 | * **deps:** upgrade libmediainfo to v25.07 ([66a1467](https://github.com/buzz/mediainfo.js/commit/66a14673015cfa2555a9853e6af47c33114c25f6)) 22 | 23 | ## [0.3.5](https://github.com/buzz/mediainfo.js/compare/v0.3.4...v0.3.5) (2025-04-02) 24 | 25 | 26 | ### Upgrades 27 | 28 | * **deps:** upgrade libmediainfo to v25.03 ([062a445](https://github.com/buzz/mediainfo.js/commit/062a44593cdf0c601e7137583ff3e1afbd9e07a9)) 29 | 30 | ## [0.3.4](https://github.com/buzz/mediainfo.js/compare/v0.3.3...v0.3.4) (2024-12-14) 31 | 32 | 33 | ### Upgrades 34 | 35 | * **deps:** upgrade libmediainfo to v24.12 ([9e79d67](https://github.com/buzz/mediainfo.js/commit/9e79d67a21284e9dc5445e017048d36223530e41)) 36 | 37 | ## [0.3.3](https://github.com/buzz/mediainfo.js/compare/v0.3.2...v0.3.3) (2024-11-13) 38 | 39 | 40 | ### Upgrades 41 | 42 | * **deps:** upgrade libmediainfo to v24.11 ([43512a4](https://github.com/buzz/mediainfo.js/commit/43512a4d958d20af971e3a77bcb90161bdc9420a)) 43 | 44 | ## [0.3.2](https://github.com/buzz/mediainfo.js/compare/v0.3.1...v0.3.2) (2024-07-11) 45 | 46 | 47 | ### Upgrades 48 | 49 | * **deps:** upgrade libmediainfo to v24.06 ([5de50e7](https://github.com/buzz/mediainfo.js/commit/5de50e7f0f9820151fda7cdeb3e78be677550ba4)) 50 | 51 | ## [0.3.1](https://github.com/buzz/mediainfo.js/compare/v0.3.0...v0.3.1) (2024-06-01) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * **deps:** support Node.js 18 ([dbbfbed](https://github.com/buzz/mediainfo.js/commit/dbbfbedc53d54bbf0e50a73be1a68cf9f01229f6)), closes [#155](https://github.com/buzz/mediainfo.js/issues/155) 57 | 58 | ## [0.3.0](https://github.com/buzz/mediainfo.js/compare/v0.2.2...v0.3.0) (2024-05-31) 59 | 60 | 61 | ### ⚠ BREAKING CHANGES 62 | 63 | * Consumers of the library need to update their types. 64 | 65 | ### Features 66 | 67 | * add interfaces and detailed descriptions for all track types ([69482a7](https://github.com/buzz/mediainfo.js/commit/69482a766f96e7ccade965d027c32e67bccf353e)) 68 | * also accept number as size argument ([4405da4](https://github.com/buzz/mediainfo.js/commit/4405da4a5347e1b3fec10af7bc52b65f81613f94)) 69 | * **build:** optimize WASM file size ([fa05a1a](https://github.com/buzz/mediainfo.js/commit/fa05a1ab684897d38b76d26211aa5fe488bd481c)) 70 | * **types:** move common track fields to `BaseTrack` ([e2b4c1a](https://github.com/buzz/mediainfo.js/commit/e2b4c1af84a09756ecb01887b3ac3e5c0719fb17)) 71 | 72 | 73 | ### Upgrades 74 | 75 | * **deps:** upgrade libmediainfo to v24.04 ([98531dd](https://github.com/buzz/mediainfo.js/commit/98531dd37def908d23e653ca9e7f3c603e08f836)) 76 | * **deps:** upgrade libmediainfo to v24.05 ([ee4ad2b](https://github.com/buzz/mediainfo.js/commit/ee4ad2b8974942402087bbac0be2aa3b96e0a126)) 77 | 78 | ## [0.2.2](https://github.com/buzz/mediainfo.js/compare/v0.2.1...v0.2.2) (2024-02-28) 79 | 80 | 81 | ### Upgrades 82 | 83 | * **deps:** upgrade libmediainfo to v24.01 ([63422ca](https://github.com/buzz/mediainfo.js/commit/63422ca1fef1295c0d4649b19f493ce1af4dc987)) 84 | 85 | ## [0.2.1](https://github.com/buzz/mediainfo.js/compare/v0.2.0...v0.2.1) (2023-08-21) 86 | 87 | 88 | ### Features 89 | 90 | * **umd:** add custom `locateFile` code that handles the CDN version (closes [#142](https://github.com/buzz/mediainfo.js/issues/142)) ([c223eb7](https://github.com/buzz/mediainfo.js/commit/c223eb7fb16355e2ae75febb183fd7107df1d77c)) 91 | 92 | 93 | ### Bug Fixes 94 | 95 | * **examples:** fix scope error in cli.ts ([cd044ff](https://github.com/buzz/mediainfo.js/commit/cd044ff82d46092eb765f2fdab7d1f8d47e824ba)), closes [/github.com/buzz/mediainfo.js/issues/136#issuecomment-1657697043](https://github.com/buzz//github.com/buzz/mediainfo.js/issues/136/issues/issuecomment-1657697043) 96 | 97 | ## [0.2.0](https://github.com/buzz/mediainfo.js/compare/v0.1.8...v0.2.0) (2023-07-28) 98 | 99 | 100 | ### ⚠ BREAKING CHANGES 101 | 102 | * **types:** Consumers of the library may need to update their 103 | type imports accordingly. 104 | 105 | ### Features 106 | 107 | * **build:** add separate builds for UMD, CJS, and ESM ([c681079](https://github.com/buzz/mediainfo.js/commit/c6810790e4daf3b2168e84c0de368090a38f6254)) 108 | * **types:** generate typings for MediaInfo result object (closes [#117](https://github.com/buzz/mediainfo.js/issues/117)) ([57ca6f3](https://github.com/buzz/mediainfo.js/commit/57ca6f3ecbf7b75cdc7f8d977268da434b0b6047)) 109 | * **types:** proper type conversion from JSON result ([9b87930](https://github.com/buzz/mediainfo.js/commit/9b879303f956bb572d83776677328dcda3ab0fdc)) 110 | 111 | ## [0.1.9](https://github.com/buzz/mediainfo.js/compare/v0.1.8...v0.1.9) (2022-12-15) 112 | 113 | ## [0.1.8](https://github.com/buzz/mediainfo.js/compare/v0.1.7...v0.1.8) (2022-07-23) 114 | 115 | ## [0.1.7](https://github.com/buzz/mediainfo.js/compare/v0.1.6...v0.1.7) (2021-10-19) 116 | 117 | ## [0.1.6](https://github.com/buzz/mediainfo.js/compare/v0.1.5...v0.1.6) (2021-09-02) 118 | 119 | ## [0.1.5](https://github.com/buzz/mediainfo.js/compare/v0.1.4...v0.1.5) (2021-03-10) 120 | 121 | ## [0.1.4](https://github.com/buzz/mediainfo.js/compare/v0.1.3...v0.1.4) (2020-08-21) 122 | 123 | ## [0.1.3](https://github.com/buzz/mediainfo.js/compare/v0.1.2...v0.1.3) (2020-08-07) 124 | 125 | ## [0.1.2](https://github.com/buzz/mediainfo.js/compare/v0.1.1...v0.1.2) (2020-06-14) 126 | 127 | ## [0.1.1](https://github.com/buzz/mediainfo.js/compare/v0.1.0...v0.1.1) (2020-06-13) 128 | 129 | ## [0.1.0](https://github.com/buzz/mediainfo.js/compare/v0.0.3...v0.1.0) (2020-06-12) 130 | 131 | ## [0.0.3](https://github.com/buzz/mediainfo.js/compare/v0.0.2...v0.0.3) (2020-03-19) 132 | 133 | ## [0.0.2](https://github.com/buzz/mediainfo.js/compare/v0.0.1...v0.0.2) (2016-12-21) 134 | 135 | ## [0.0.1](https://github.com/buzz/mediainfo.js/tree/v0.0.1) (2016-02-21) 136 | -------------------------------------------------------------------------------- /__tests__/dwsample_mp4_360p.mp4.test.ts: -------------------------------------------------------------------------------- 1 | import { analyzeFile, expectToBeDefined, expectTrackType, fixturePath } from './utils.ts' 2 | 3 | const filePath = fixturePath('dwsample mp4 360p.mp4') 4 | 5 | it('should parse file', async () => { 6 | const result = await analyzeFile(filePath) 7 | expectToBeDefined(result.media) 8 | 9 | const { track } = result.media 10 | expect(track).toHaveLength(3) 11 | const [track0, track1, track2] = track 12 | 13 | expectTrackType(track0, 'General') 14 | expect(track0.Format).toBe('MPEG-4') 15 | expect(track0.Format_Profile).toBe('Base Media') 16 | expect(track0.CodecID).toBe('isom') 17 | expect(track0.CodecID_Compatible).toBe('isom/iso2/avc1/mp41') 18 | expect(track0.FileSize).toBe('4553606') 19 | expect(track0.Duration).toBeCloseTo(53.76) 20 | expect(track0.OverallBitRate).toBeNear(677_620, 2) 21 | expect(track0.FrameRate).toBeCloseTo(25) 22 | expect(track0.FrameCount).toBe(1344) 23 | expect(track0.StreamSize).toBe(46_509) 24 | expect(track0.HeaderSize).toBe(40) 25 | expect(track0.DataSize).toBe(4_507_105) 26 | expect(track0.FooterSize).toBe(46_461) 27 | expect(track0.IsStreamable).toBe('No') 28 | 29 | expectTrackType(track1, 'Video') 30 | expect(track1.StreamOrder).toBe('0') 31 | expect(track1.ID).toBe('1') 32 | expect(track1.Format).toBe('AVC') 33 | expect(track1.Format_Profile).toBe('High') 34 | expect(track1.Format_Level).toBe('3') 35 | expect(track1.Format_Settings_CABAC).toBe('Yes') 36 | expect(track1.Format_Settings_RefFrames).toBe(4) 37 | expect(track1.CodecID).toBe('avc1') 38 | expect(track1.Duration).toBeCloseTo(53.76) 39 | expect(track1.BitRate).toBeNear(558_000, 2) 40 | expect(track1.Width).toBe(636) 41 | expect(track1.Height).toBe(360) 42 | expect(track1.Stored_Height).toBe(368) 43 | expect(track1.Sampled_Width).toBe(636) 44 | expect(track1.PixelAspectRatio).toBeCloseTo(1) 45 | expect(track1.DisplayAspectRatio).toBeCloseTo(1.767) 46 | expect(track1.Rotation).toBe('0.000') 47 | expect(track1.FrameRate_Mode).toBe('CFR') 48 | expect(track1.FrameRate_Mode_Original).toBe('VFR') 49 | expect(track1.FrameRate).toBeCloseTo(25) 50 | expect(track1.FrameCount).toBe(1344) 51 | expect(track1.ColorSpace).toBe('YUV') 52 | expect(track1.ChromaSubsampling).toBe('4:2:0') 53 | expect(track1.BitDepth).toBe(8) 54 | expect(track1.ScanType).toBe('Progressive') 55 | expect(track1.StreamSize).toBe(3_645_916) 56 | expect(track1.Encoded_Library).toBe('x264 - core 155 r2917 0a84d98') 57 | expect(track1.Encoded_Library_Name).toBe('x264') 58 | expect(track1.Encoded_Library_Version).toBe('core 155 r2917 0a84d98') 59 | expect(track1.Encoded_Library_Settings).toBe( 60 | 'cabac=1 / ref=3 / deblock=1:0:0 / analyse=0x3:0x113 / me=hex / subme=7 / psy=1 / psy_rd=1.00:0.00 / mixed_ref=1 / me_range=16 / chroma_me=1 / trellis=1 / 8x8dct=1 / cqm=0 / deadzone=21,11 / fast_pskip=1 / chroma_qp_offset=-2 / threads=11 / lookahead_threads=1 / sliced_threads=0 / nr=0 / decimate=1 / interlaced=0 / bluray_compat=0 / constrained_intra=0 / bframes=3 / b_pyramid=2 / b_adapt=1 / b_bias=0 / direct=1 / weightb=1 / open_gop=0 / weightp=2 / keyint=250 / keyint_min=25 / scenecut=40 / intra_refresh=0 / rc_lookahead=40 / rc=abr / mbtree=1 / bitrate=558 / ratetol=1.0 / qcomp=0.60 / qpmin=0 / qpmax=69 / qpstep=4 / ip_ratio=1.40 / aq=1:1.00' 61 | ) 62 | if (track1.extra === undefined) { 63 | throw new Error('Expected extra data on track') 64 | } 65 | expect(track1.extra.CodecConfigurationBox).toBe('avcC') 66 | 67 | expectTrackType(track2, 'Audio') 68 | expect(track2.StreamOrder).toBe('1') 69 | expect(track2.ID).toBe('2') 70 | expect(track2.Format).toBe('AAC') 71 | expect(track2.Format_AdditionalFeatures).toBe('LC') 72 | expect(track2.CodecID).toBe('2 / 40 / mp4a-40-2') 73 | expect(track2.Duration).toBeCloseTo(53.76) 74 | expect(track2.BitRate_Mode).toBe('CBR') 75 | expect(track2.BitRate).toBeNear(128_041, 2) 76 | expect(track2.Channels).toBe(2) 77 | expect(track2.ChannelPositions).toBe('Front: L R') 78 | expect(track2.ChannelLayout).toBe('L R') 79 | expect(track2.SamplesPerFrame).toBeCloseTo(1024) 80 | expect(track2.SamplingRate).toBeCloseTo(44_100) 81 | expect(track2.SamplingCount).toBe(2_370_816) 82 | expect(track2.FrameRate).toBeCloseTo(43.066) 83 | expect(track2.FrameCount).toBe(2315) 84 | expect(track2.Compression_Mode).toBe('Lossy') 85 | expect(track2.StreamSize).toBe(860_067) 86 | }) 87 | 88 | it('should return full data', async () => { 89 | const result = await analyzeFile(filePath, { full: true }) 90 | expectToBeDefined(result.media) 91 | 92 | const { track } = result.media 93 | const [track0, track1, track2] = track 94 | 95 | expectTrackType(track0, 'General') 96 | expect(track0.InternetMediaType).toBe('video/mp4') 97 | expect(track0.FileSize).toBe('4553606') 98 | expect(track0.FileSize_String).toBe('4.34 MiB') 99 | expect(track0.Duration).toBe(53.76) 100 | expect(track0.Duration_String).toBe('53 s 760 ms') 101 | expect(track0.OverallBitRate).toBe(677_620) 102 | expect(track0.OverallBitRate_String).toBe('678 kb/s') 103 | expect(track0.StreamSize).toBe(46_509) 104 | expect(track0.StreamSize_String).toBe('45.4 KiB (1%)') 105 | 106 | expectTrackType(track1, 'Video') 107 | expect(track1.Format_Info).toBe('Advanced Video Codec') 108 | expect(track1.Format_Url).toBe('http://developers.videolan.org/x264.html') 109 | expect(track1.InternetMediaType).toBe('video/H264') 110 | expect(track1.CodecID_Info).toBe('Advanced Video Coding') 111 | expect(track1.DisplayAspectRatio_String).toBe('16:9') 112 | expect(track1.StreamSize_String).toBe('3.48 MiB (80%)') 113 | 114 | expectTrackType(track2, 'Audio') 115 | expect(track2.Format_Info).toBe('Advanced Audio Codec Low Complexity') 116 | expect(track2.BitRate_Mode_String).toBe('Constant') 117 | expect(track2.Channels_String).toBe('2 channels') 118 | expect(track2.ChannelPositions_String2).toBe('2/0/0') 119 | expect(track2.SamplingRate_String).toBe('44.1 kHz') 120 | }) 121 | 122 | it('should not return full data', async () => { 123 | const result = await analyzeFile(filePath, { full: false }) 124 | expectToBeDefined(result.media) 125 | 126 | const { track } = result.media 127 | const [track0, track1, track2] = track 128 | 129 | expectTrackType(track0, 'General') 130 | expect(track0.InternetMediaType).not.toBeDefined() 131 | expect(track0.FileSize_String).not.toBeDefined() 132 | expect(track0.Duration_String).not.toBeDefined() 133 | expect(track0.OverallBitRate_String).not.toBeDefined() 134 | expect(track0.StreamSize_String).not.toBeDefined() 135 | 136 | expectTrackType(track1, 'Video') 137 | expect(track1.Format_Info).not.toBeDefined() 138 | expect(track1.Format_Url).not.toBeDefined() 139 | expect(track1.InternetMediaType).not.toBeDefined() 140 | expect(track1.CodecID_Info).not.toBeDefined() 141 | expect(track1.DisplayAspectRatio_String).not.toBeDefined() 142 | expect(track1.StreamSize_String).not.toBeDefined() 143 | 144 | expectTrackType(track2, 'Audio') 145 | expect(track2.Format_Info).not.toBeDefined() 146 | expect(track2.BitRate_Mode_String).not.toBeDefined() 147 | expect(track2.Channels_String).not.toBeDefined() 148 | expect(track2.ChannelPositions_String2).not.toBeDefined() 149 | expect(track2.SamplingRate_String).not.toBeDefined() 150 | }) 151 | -------------------------------------------------------------------------------- /src/MediaInfo.ts: -------------------------------------------------------------------------------- 1 | import { unknownToError } from './error.js' 2 | import { FLOAT_FIELDS, INT_FIELDS, type MediaInfoResult, type Track } from './MediaInfoResult.js' 3 | import type { MediaInfoFactoryOptions } from './mediaInfoFactory.js' 4 | import type { MediaInfoModule, MediaInfoWasmInterface } from './MediaInfoModule.js' 5 | 6 | const MAX_UINT32_PLUS_ONE = 2 ** 32 7 | 8 | /** Format of the result type */ 9 | type FormatType = 'object' | 'JSON' | 'XML' | 'HTML' | 'text' 10 | 11 | type MediaInfoOptions = Required< 12 | Omit, 'locateFile'> 13 | > 14 | 15 | type SizeArg = (() => Promise | number) | number 16 | 17 | type ReadChunkFunc = (size: number, offset: number) => Promise | Uint8Array 18 | 19 | interface ResultMap { 20 | object: MediaInfoResult 21 | JSON: string 22 | XML: string 23 | HTML: string 24 | text: string 25 | } 26 | 27 | const FORMAT_CHOICES = ['JSON', 'XML', 'HTML', 'text'] as const 28 | 29 | const DEFAULT_OPTIONS = { 30 | coverData: false, 31 | chunkSize: 256 * 1024, 32 | format: 'object', 33 | full: false, 34 | } as const 35 | 36 | type ResultCallback = ( 37 | result: ResultMap[TFormat] | null, 38 | err?: unknown 39 | ) => void 40 | 41 | /** 42 | * Wrapper for the MediaInfoLib WASM module. 43 | * 44 | * This class should not be instantiated directly. Use the {@link mediaInfoFactory} function 45 | * to create instances of `MediaInfo`. 46 | * 47 | * @typeParam TFormat - The format type, defaults to `object`. 48 | */ 49 | class MediaInfo { 50 | private readonly mediainfoModule: MediaInfoModule 51 | private mediainfoModuleInstance: MediaInfoWasmInterface 52 | private isAnalyzing = false 53 | 54 | /** @group General Use */ 55 | readonly options: MediaInfoOptions 56 | 57 | /** 58 | * The constructor should not be called directly, instead use {@link mediaInfoFactory}. 59 | * 60 | * @hidden 61 | * @param mediainfoModule WASM module 62 | * @param options User options 63 | */ 64 | constructor(mediainfoModule: MediaInfoModule, options: MediaInfoOptions) { 65 | this.mediainfoModule = mediainfoModule 66 | this.options = options 67 | this.mediainfoModuleInstance = this.instantiateModuleInstance() 68 | } 69 | 70 | /** 71 | * Convenience method for analyzing a buffer chunk by chunk. 72 | * 73 | * @param size Return total buffer size in bytes. 74 | * @param readChunk Read chunk of data and return an {@link Uint8Array}. 75 | * @group General Use 76 | */ 77 | analyzeData(size: SizeArg, readChunk: ReadChunkFunc): Promise 78 | 79 | /** 80 | * Convenience method for analyzing a buffer chunk by chunk. 81 | * 82 | * @param size Return total buffer size in bytes. 83 | * @param readChunk Read chunk of data and return an {@link Uint8Array}. 84 | * @param callback Function that is called once the processing is done 85 | * @group General Use 86 | */ 87 | analyzeData(size: SizeArg, readChunk: ReadChunkFunc, callback: ResultCallback): void 88 | 89 | analyzeData( 90 | size: SizeArg, 91 | readChunk: ReadChunkFunc, 92 | callback?: ResultCallback 93 | ): Promise | undefined { 94 | // Support promise signature 95 | if (callback === undefined) { 96 | return new Promise((resolve, reject) => { 97 | const resultCb: ResultCallback = (result, error) => { 98 | this.isAnalyzing = false 99 | if (error || !result) { 100 | reject(unknownToError(error)) 101 | } else { 102 | resolve(result) 103 | } 104 | } 105 | this.analyzeData(size, readChunk, resultCb) 106 | }) 107 | } 108 | 109 | if (this.isAnalyzing) { 110 | callback('', new Error('cannot start a new analysis while another is in progress')) 111 | return 112 | } 113 | this.reset() 114 | this.isAnalyzing = true 115 | 116 | const finalize = () => { 117 | try { 118 | this.openBufferFinalize() 119 | const result = this.inform() 120 | if (this.options.format === 'object') { 121 | callback(this.parseResultJson(result)) 122 | } else { 123 | callback(result) 124 | } 125 | } finally { 126 | this.isAnalyzing = false 127 | } 128 | } 129 | 130 | let offset = 0 131 | const runReadDataLoop = (fileSize: number) => { 132 | const readNextChunk = (data: Uint8Array) => { 133 | if (continueBuffer(data)) { 134 | getChunk() 135 | } else { 136 | finalize() 137 | } 138 | } 139 | 140 | const getChunk = () => { 141 | let dataValue 142 | try { 143 | const safeSize = Math.min(this.options.chunkSize, fileSize - offset) 144 | dataValue = readChunk(safeSize, offset) 145 | } catch (error: unknown) { 146 | this.isAnalyzing = false 147 | callback('', unknownToError(error)) 148 | return 149 | } 150 | 151 | if (dataValue instanceof Promise) { 152 | dataValue.then(readNextChunk).catch((error: unknown) => { 153 | this.isAnalyzing = false 154 | callback('', unknownToError(error)) 155 | }) 156 | } else { 157 | readNextChunk(dataValue) 158 | } 159 | } 160 | 161 | const continueBuffer = (data: Uint8Array): boolean => { 162 | if (data.length === 0 || this.openBufferContinue(data, data.length)) { 163 | return false 164 | } 165 | const seekTo: number = this.openBufferContinueGotoGet() 166 | if (seekTo === -1) { 167 | offset += data.length 168 | } else { 169 | offset = seekTo 170 | this.openBufferInit(fileSize, seekTo) 171 | } 172 | return true 173 | } 174 | 175 | this.openBufferInit(fileSize, offset) 176 | getChunk() 177 | } 178 | 179 | const fileSizeValue = typeof size === 'function' ? size() : size 180 | 181 | if (fileSizeValue instanceof Promise) { 182 | fileSizeValue.then(runReadDataLoop).catch((error: unknown) => { 183 | callback(null, unknownToError(error)) 184 | }) 185 | } else { 186 | runReadDataLoop(fileSizeValue) 187 | } 188 | } 189 | 190 | /** 191 | * Close the MediaInfoLib WASM instance. 192 | * 193 | * @group General Use 194 | */ 195 | close(): void { 196 | if (typeof this.mediainfoModuleInstance.close === 'function') { 197 | this.mediainfoModuleInstance.close() 198 | } 199 | } 200 | 201 | /** 202 | * Reset the MediaInfoLib WASM instance to its initial state. 203 | * 204 | * This method ensures that the instance is ready for a new parse. 205 | * @group General Use 206 | */ 207 | reset(): void { 208 | this.mediainfoModuleInstance.delete() 209 | this.mediainfoModuleInstance = this.instantiateModuleInstance() 210 | } 211 | 212 | /** 213 | * Receive result data from the WASM instance. 214 | * 215 | * (This is a low-level MediaInfoLib function.) 216 | * 217 | * @returns Result data (format can be configured in options) 218 | * @group Low-level 219 | */ 220 | inform(): string { 221 | return this.mediainfoModuleInstance.inform() 222 | } 223 | 224 | /** 225 | * Send more data to the WASM instance. 226 | * 227 | * (This is a low-level MediaInfoLib function.) 228 | * 229 | * @param data Data buffer 230 | * @param size Buffer size 231 | * @returns Processing state: `0` (no bits set) = not finished, Bit `0` set = enough data read for providing information 232 | * @group Low-level 233 | */ 234 | openBufferContinue(data: Uint8Array, size: number): boolean { 235 | // bit 3 set -> done 236 | return !!(this.mediainfoModuleInstance.open_buffer_continue(data, size) & 0x08) 237 | } 238 | 239 | /** 240 | * Retrieve seek position from WASM instance. 241 | * The MediaInfoLib function `Open_Buffer_GoTo` returns an integer with 64 bit precision. 242 | * It would be cut at 32 bit due to the JavaScript bindings. Here we transport the low and high 243 | * parts separately and put them together. 244 | * 245 | * (This is a low-level MediaInfoLib function.) 246 | * 247 | * @returns Seek position (where MediaInfoLib wants go in the data buffer) 248 | * @group Low-level 249 | */ 250 | openBufferContinueGotoGet(): number { 251 | // JS bindings don't support 64 bit int 252 | // https://github.com/buzz/mediainfo.js/issues/11 253 | let seekTo = -1 254 | const seekToLow: number = this.mediainfoModuleInstance.open_buffer_continue_goto_get_lower() 255 | const seekToHigh: number = this.mediainfoModuleInstance.open_buffer_continue_goto_get_upper() 256 | if (seekToLow == -1 && seekToHigh == -1) { 257 | seekTo = -1 258 | } else if (seekToLow < 0) { 259 | seekTo = seekToLow + MAX_UINT32_PLUS_ONE + seekToHigh * MAX_UINT32_PLUS_ONE 260 | } else { 261 | seekTo = seekToLow + seekToHigh * MAX_UINT32_PLUS_ONE 262 | } 263 | return seekTo 264 | } 265 | 266 | /** 267 | * Inform MediaInfoLib that no more data is being read. 268 | * 269 | * (This is a low-level MediaInfoLib function.) 270 | * 271 | * @group Low-level 272 | */ 273 | openBufferFinalize(): void { 274 | this.mediainfoModuleInstance.open_buffer_finalize() 275 | } 276 | 277 | /** 278 | * Prepare MediaInfoLib to process a data buffer. 279 | * 280 | * (This is a low-level MediaInfoLib function.) 281 | * 282 | * @param size Expected buffer size 283 | * @param offset Buffer offset 284 | * @group Low-level 285 | */ 286 | openBufferInit(size: number, offset: number): void { 287 | this.mediainfoModuleInstance.open_buffer_init(size, offset) 288 | } 289 | 290 | /** 291 | * Parse result JSON. Convert integer/float fields. 292 | * 293 | * @param result Serialized JSON from MediaInfo 294 | * @returns Parsed JSON object 295 | */ 296 | private parseResultJson(resultString: string): ResultMap[TFormat] { 297 | type Writable = { -readonly [P in keyof T]: T[P] } 298 | 299 | const intFields = INT_FIELDS as readonly string[] 300 | const floatFields = FLOAT_FIELDS as readonly string[] 301 | 302 | // Parse JSON 303 | const result = JSON.parse(resultString) as MediaInfoResult 304 | 305 | if (result.media) { 306 | const newMedia = { ...result.media, track: [] as Writable[] } 307 | 308 | if (Array.isArray(result.media.track)) { 309 | for (const track of result.media.track) { 310 | let newTrack: Writable = { '@type': track['@type'] } 311 | for (const [key, val] of Object.entries(track) as [string, unknown][]) { 312 | if (key === '@type') { 313 | continue 314 | } 315 | if (typeof val === 'string' && intFields.includes(key)) { 316 | newTrack = { ...newTrack, [key]: Number.parseInt(val, 10) } 317 | } else if (typeof val === 'string' && floatFields.includes(key)) { 318 | newTrack = { ...newTrack, [key]: Number.parseFloat(val) } 319 | } else { 320 | newTrack = { ...newTrack, [key]: val } 321 | } 322 | } 323 | newMedia.track.push(newTrack) 324 | } 325 | } 326 | 327 | return { ...result, media: newMedia } as ResultMap[TFormat] 328 | } 329 | 330 | return result as ResultMap[TFormat] 331 | } 332 | 333 | /** 334 | * Instantiate a new WASM module instance. 335 | * 336 | * @returns MediaInfo module instance 337 | */ 338 | private instantiateModuleInstance(): MediaInfoWasmInterface { 339 | return new this.mediainfoModule.MediaInfo( 340 | this.options.format === 'object' ? 'JSON' : this.options.format, 341 | this.options.coverData, 342 | this.options.full 343 | ) 344 | } 345 | } 346 | 347 | export type { FormatType, ReadChunkFunc, ResultMap, SizeArg } 348 | export { DEFAULT_OPTIONS, FORMAT_CHOICES } 349 | export default MediaInfo 350 | --------------------------------------------------------------------------------