initial commit

Signed-off-by: ale <ale@manalejandro.com>
Este commit está contenido en:
ale
2025-09-06 19:11:40 +02:00
commit b8c6d238bc
Se han modificado 13 ficheros con 1940 adiciones y 0 borrados

68
.gitignore vendido Archivo normal
Ver fichero

@@ -0,0 +1,68 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.lock
*-lock.json
# Package files
*.tgz
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Coverage directory used by tools like istanbul
coverage/
.nyc_output/
# Editor directories and files
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Temporary files and directories
temp/
tmp/
*.tmp
*.temp.*
# Test files and example outputs
test_*.i2m
example_*.i2m
*_extracted*
*.test.js
*.spec.js
# Build directories
dist/
build/

46
.npmignore Archivo normal
Ver fichero

@@ -0,0 +1,46 @@
# Exclude development files
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Exclude test files and examples that aren't needed in the package
test/
tests/
*.test.js
*.spec.js
# Exclude development configuration
.nyc_output/
coverage/
# Exclude OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Exclude editor files
.vscode/
.idea/
*.swp
*.swo
*~
# Exclude temporary files
temp/
tmp/
*.tmp
# Exclude log files
*.log
# Exclude environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

45
CHANGELOG.md Archivo normal
Ver fichero

@@ -0,0 +1,45 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.0.0] - 2025-09-06
### Added
- Initial release of IMG2MP3
- MP3 encoding into images using steganography techniques
- Decoding of .i2m files back to original image and MP3
- Built-in audio player with support for multiple system players:
- mpg123, mpv, ffplay, vlc, sox
- Command-line interface with the following commands:
- `encode`: Embed MP3 into image
- `decode`: Extract image and MP3 from .i2m file
- `play`: Play audio from .i2m file
- `info`: Show information about .i2m file
- `interactive`: Interactive mode for guided usage
- Programmatic API with IMG2MP3 class
- TypeScript type definitions
- Cross-platform support (Linux, macOS, Windows)
- Custom .i2m file format with magic byte verification
- Automatic cleanup of temporary files
- Comprehensive documentation and examples
### Features
- **Steganography**: Advanced encoding techniques to hide MP3 data in images
- **Lossless**: Original image and audio quality preserved
- **Format Support**: Works with various image formats (PNG, JPEG, WebP, etc.)
- **Audio Players**: Automatic detection of available system audio players
- **CLI Interface**: Easy-to-use command-line tools
- **Interactive Mode**: Guided step-by-step usage
- **API**: Clean programmatic interface for integration
- **TypeScript**: Full type definitions included
### Technical Details
- Uses Sharp library for reliable image processing
- Custom container format with 16-byte header
- Magic bytes "I2M3" for file format verification
- Supports images and MP3 files of any size
- Generates PNG containers for pixel manipulation reliability
- Temporary file management with automatic cleanup

21
LICENSE Archivo normal
Ver fichero

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 ale
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

235
PUBLISHING.md Archivo normal
Ver fichero

@@ -0,0 +1,235 @@
# Publishing to NPM
This document provides step-by-step instructions for publishing the IMG2MP3 package to NPM.
## Prerequisites
1. **NPM Account**: Ensure you have an NPM account at [npmjs.com](https://www.npmjs.com/)
2. **NPM CLI**: Make sure npm is installed and up to date
3. **Authentication**: You need to be logged in to npm
## Pre-publication Steps
### 1. Verify Package Information
Check that all package information is correct:
```bash
# Review package.json
cat package.json
# Check package structure
npm pack --dry-run
```
### 2. Test the Package Locally
```bash
# Install dependencies
npm install
# Test CLI commands
node bin/cli.js --help
node bin/cli.js info --help
# Test the API
node examples/basic_usage.js
```
### 3. Check for Available Package Name
```bash
# Check if package name is available
npm view img2mp3
# If the package exists, you'll need to choose a different name
# Consider variations like: img2mp3-steganography, img-to-mp3, etc.
```
### 4. Verify Package Contents
```bash
# See what files will be included in the package
npm pack --dry-run
# The output should include:
# - src/
# - bin/
# - examples/
# - index.d.ts
# - README.md
# - LICENSE
# - package.json
```
## Publishing Steps
### 1. Login to NPM
```bash
npm login
```
Enter your NPM credentials when prompted.
### 2. Verify Login
```bash
npm whoami
```
This should display your NPM username.
### 3. Publish the Package
For first-time publication:
```bash
npm publish
```
If the package name is already taken, you can:
- Choose a scoped package name: `npm publish --access public`
- Or update the name in package.json and try again
### 4. Verify Publication
After successful publication:
```bash
# Check the package on NPM
npm view img2mp3
# Test installation
npm install -g img2mp3
# Test global installation
img2mp3 --help
```
## Post-publication Steps
### 1. Create Git Repository
If you haven't already:
```bash
git init
git add .
git commit -m "Initial release v1.0.0"
git tag v1.0.0
# Add remote and push (update URL with your repo)
git remote add origin https://github.com/ale/img2mp3.git
git push -u origin main
git push --tags
```
### 2. Update Repository Links
Ensure the repository URLs in package.json point to your actual repository.
### 3. Documentation
- Update the README.md with the correct installation instructions
- Add any platform-specific installation notes
- Include examples and screenshots if possible
## Updating the Package
For future updates:
### 1. Update Version
```bash
# For patch updates (bug fixes)
npm version patch
# For minor updates (new features)
npm version minor
# For major updates (breaking changes)
npm version major
```
### 2. Update Changelog
Update CHANGELOG.md with the new changes.
### 3. Publish Update
```bash
npm publish
```
### 4. Tag the Release
```bash
git push --tags
```
## Package Name Alternatives
If "img2mp3" is already taken, consider these alternatives:
- `img2mp3-encoder`
- `image-to-mp3`
- `mp3-steganography`
- `img2audio`
- `steganography-mp3`
- `audio-image-encoder`
- `img-audio-hide`
- `@yourname/img2mp3` (scoped package)
## Troubleshooting
### Package Name Already Exists
```bash
# Option 1: Use a scoped package
# Update package.json name to "@yourusername/img2mp3"
npm publish --access public
# Option 2: Choose a different name
# Update package.json name field and try again
```
### Authentication Issues
```bash
# Clear NPM cache
npm cache clean --force
# Re-login
npm logout
npm login
```
### Permission Errors
```bash
# Check if you're added to the package as collaborator
npm owner ls img2mp3
# Add yourself if needed (package owner must do this)
npm owner add yourusername img2mp3
```
## Success Verification
After successful publication, your package should be:
1. **Searchable**: Available at https://www.npmjs.com/package/img2mp3
2. **Installable**: `npm install -g img2mp3` works
3. **Executable**: `img2mp3 --help` shows help text
4. **Functional**: Basic encode/decode operations work
## Marketing Your Package
1. **GitHub README**: Ensure your GitHub repository has a comprehensive README
2. **Keywords**: Good keywords in package.json help discoverability
3. **Documentation**: Complete API documentation and examples
4. **Social Media**: Share your package on relevant communities
5. **Blog Post**: Write about the technical implementation and use cases
Remember to respect copyright laws and include appropriate disclaimers about the intended use of steganography tools.

387
README.md Archivo normal
Ver fichero

@@ -0,0 +1,387 @@
# IMG2MP3 🎵🖼️
A powerful Node.js tool for encoding MP3 files into images and decoding them back, with a built-in player for audio-embedded images.
## Features
- **🔐 Steganography**: Hide MP3 files inside images using advanced encoding techniques
- **🎵 Audio Player**: Built-in player for `.i2m` files with support for multiple audio backends
- **🖼️ Image Support**: Works with various image formats (PNG, JPEG, WebP, etc.)
- **⚡ CLI Interface**: Easy-to-use command-line interface with interactive mode
- **📊 File Analysis**: Get detailed information about encoded files
- **🧹 Clean API**: Simple programmatic interface for integration into other projects
## Installation
### Global Installation (Recommended)
```bash
npm install -g img2mp3
```
### Local Installation
```bash
npm install img2mp3
```
## Usage
### Command Line Interface
#### Encode MP3 into Image
```bash
# Basic encoding
img2mp3 encode image.jpg song.mp3
# Specify output file
img2mp3 encode image.jpg song.mp3 output.i2m
# Verbose output
img2mp3 encode image.jpg song.mp3 -v
```
#### Decode .i2m File
```bash
# Extract both image and MP3
img2mp3 decode song.i2m
# Specify output paths
img2mp3 decode song.i2m -i extracted_image.png -m extracted_song.mp3
```
#### Play .i2m File
```bash
# Play audio from image
img2mp3 play song.i2m
# Play with verbose output
img2mp3 play song.i2m -v
```
#### Get File Information
```bash
# Show detailed information about .i2m file
img2mp3 info song.i2m
```
#### Interactive Mode
```bash
# Start interactive mode for guided usage
img2mp3 interactive
# or
img2mp3 i
```
### Programmatic Usage
```javascript
const { IMG2MP3, encode, decode, play } = require('img2mp3');
// Using the main class
const img2mp3 = new IMG2MP3();
// Encode MP3 into image
async function encodeExample() {
try {
const result = await img2mp3.encode('image.jpg', 'song.mp3', 'output.i2m');
console.log('Encoding successful:', result);
} catch (error) {
console.error('Encoding failed:', error.message);
}
}
// Decode .i2m file
async function decodeExample() {
try {
const result = await img2mp3.decode('output.i2m', 'extracted.png', 'extracted.mp3');
console.log('Decoding successful:', result);
} catch (error) {
console.error('Decoding failed:', error.message);
}
}
// Play .i2m file
async function playExample() {
try {
await img2mp3.play('output.i2m');
console.log('Playback finished');
} catch (error) {
console.error('Playback failed:', error.message);
}
}
// Get file information
async function infoExample() {
try {
const info = await img2mp3.getInfo('output.i2m');
if (info.isValid) {
console.log('File info:', info);
} else {
console.error('Invalid file:', info.error);
}
} catch (error) {
console.error('Analysis failed:', error.message);
}
}
// Using convenience functions
async function quickExample() {
// Quick encode
await encode('image.jpg', 'song.mp3', 'quick.i2m');
// Quick decode
await decode('quick.i2m', 'quick_img.png', 'quick_song.mp3');
// Quick play
await play('quick.i2m');
}
```
## How It Works
IMG2MP3 uses advanced steganography techniques to embed MP3 data into images:
1. **Encoding Process**:
- Reads the source image and MP3 file
- Creates a header with magic bytes and metadata
- Combines header + original image + MP3 data
- Generates a new container image to hold all data
- Saves as `.i2m` file (Image-to-MP3 format)
2. **Decoding Process**:
- Reads the `.i2m` file
- Verifies magic bytes to ensure valid format
- Extracts header information
- Separates original image and MP3 data
- Saves extracted files
3. **Playback Process**:
- Extracts MP3 data to temporary file
- Uses system audio players for playback
- Cleans up temporary files automatically
## Audio Player Support
The built-in player supports multiple audio backends:
- **mpg123** (Recommended for MP3)
- **mpv** (Universal media player)
- **ffplay** (FFmpeg player)
- **cvlc** (VLC command-line)
- **play** (SOX audio player)
The tool automatically detects available players and uses the best one.
### Installing Audio Players
#### Ubuntu/Debian:
```bash
sudo apt-get install mpg123 mpv ffmpeg vlc sox
```
#### macOS (with Homebrew):
```bash
brew install mpg123 mpv ffmpeg vlc sox
```
#### Windows:
- Download and install [mpv](https://mpv.io/installation/)
- Or install [VLC media player](https://www.videolan.org/vlc/)
## File Format (.i2m)
The `.i2m` (Image-to-MP3) format is a custom container that preserves both the original image and embedded MP3:
```
[Header - 16 bytes]
├── Magic bytes: "I2M3" (4 bytes)
├── MP3 size: uint32 (4 bytes)
├── Original image size: uint32 (4 bytes)
└── Image width: uint32 (4 bytes)
[Original Image Data]
├── Complete original image file
└── Preserves original format and quality
[MP3 Data]
├── Complete MP3 file
└── Preserves original audio quality
[Container Image]
├── PNG format for reliable pixel manipulation
└── Visually appears as a normal image
```
## API Reference
### IMG2MP3 Class
#### Constructor
```javascript
const img2mp3 = new IMG2MP3();
```
#### Methods
##### encode(imagePath, mp3Path, outputPath)
Encodes MP3 data into an image.
**Parameters:**
- `imagePath` (string): Path to source image
- `mp3Path` (string): Path to MP3 file
- `outputPath` (string): Path for output .i2m file
**Returns:** Promise\<Object\> with encoding results
##### decode(i2mPath, outputImagePath, outputMp3Path)
Decodes .i2m file back to image and MP3.
**Parameters:**
- `i2mPath` (string): Path to .i2m file
- `outputImagePath` (string): Path for extracted image
- `outputMp3Path` (string): Path for extracted MP3
**Returns:** Promise\<Object\> with decoding results
##### getInfo(i2mPath)
Gets information about .i2m file.
**Parameters:**
- `i2mPath` (string): Path to .i2m file
**Returns:** Promise\<Object\> with file information
##### play(i2mPath, options)
Plays audio from .i2m file.
**Parameters:**
- `i2mPath` (string): Path to .i2m file
- `options` (Object): Playback options (optional)
**Returns:** Promise\<void\>
##### stop()
Stops current playback.
##### getPlaybackStatus()
Gets current playback status.
**Returns:** Object with playback information
##### cleanup()
Cleans up temporary files.
## Examples
### Basic Example
```javascript
const { IMG2MP3 } = require('img2mp3');
async function example() {
const img2mp3 = new IMG2MP3();
// Encode
console.log('Encoding...');
await img2mp3.encode('photo.jpg', 'music.mp3', 'hidden.i2m');
// Get info
const info = await img2mp3.getInfo('hidden.i2m');
console.log(`Embedded ${info.mp3Size} bytes of audio`);
// Play
console.log('Playing...');
await img2mp3.play('hidden.i2m');
// Decode
console.log('Extracting...');
await img2mp3.decode('hidden.i2m', 'restored.jpg', 'restored.mp3');
}
example().catch(console.error);
```
### Batch Processing
```javascript
const fs = require('fs');
const path = require('path');
const { encode } = require('img2mp3');
async function batchEncode() {
const imageDir = './images';
const audioDir = './audio';
const outputDir = './encoded';
const images = fs.readdirSync(imageDir).filter(f =>
f.endsWith('.jpg') || f.endsWith('.png')
);
const audioFiles = fs.readdirSync(audioDir).filter(f =>
f.endsWith('.mp3')
);
for (let i = 0; i < Math.min(images.length, audioFiles.length); i++) {
const imagePath = path.join(imageDir, images[i]);
const audioPath = path.join(audioDir, audioFiles[i]);
const outputPath = path.join(outputDir, `encoded_${i}.i2m`);
try {
console.log(`Encoding ${images[i]} + ${audioFiles[i]}...`);
await encode(imagePath, audioPath, outputPath);
console.log(`✓ Created ${outputPath}`);
} catch (error) {
console.error(`✗ Failed to encode ${images[i]}: ${error.message}`);
}
}
}
batchEncode();
```
## Requirements
- **Node.js**: >= 14.0.0
- **System**: Linux, macOS, or Windows
- **Audio Player**: At least one supported audio player for playback
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## License
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
## Changelog
### v1.0.0
- Initial release
- MP3 encoding/decoding in images
- Built-in audio player
- CLI interface with interactive mode
- Cross-platform support
## Support
If you encounter any issues or have questions:
1. Check the [Issues](https://github.com/ale/img2mp3/issues) page
2. Create a new issue with detailed information
3. Include your system information and error messages
## Disclaimer
This tool is for educational and legitimate use cases only. Please respect copyright laws and only embed audio that you have the right to use.

425
bin/cli.js Archivo ejecutable
Ver fichero

@@ -0,0 +1,425 @@
#!/usr/bin/env node
const { Command } = require('commander');
const chalk = require('chalk');
const ora = require('ora');
const inquirer = require('inquirer');
const fs = require('fs');
const path = require('path');
const { IMG2MP3 } = require('../src/index');
const program = new Command();
const img2mp3 = new IMG2MP3();
// Package info
const packageJson = require('../package.json');
program
.name('img2mp3')
.description('Encode MP3 files into images and decode them back, with built-in player')
.version(packageJson.version);
/**
* Encode command
*/
program
.command('encode')
.description('Encode MP3 file into an image, creating a .i2m file')
.argument('<image>', 'Source image file')
.argument('<mp3>', 'MP3 file to embed')
.argument('[output]', 'Output .i2m file (optional)')
.option('-v, --verbose', 'Show detailed output')
.action(async (image, mp3, output, options) => {
try {
// Validate input files
if (!fs.existsSync(image)) {
console.error(chalk.red(`Error: Image file '${image}' not found`));
process.exit(1);
}
if (!fs.existsSync(mp3)) {
console.error(chalk.red(`Error: MP3 file '${mp3}' not found`));
process.exit(1);
}
// Generate output filename if not provided
if (!output) {
const baseName = path.basename(mp3, path.extname(mp3));
output = `${baseName}.i2m`;
}
// Ensure .i2m extension
if (!output.endsWith('.i2m')) {
output += '.i2m';
}
const spinner = ora('Encoding MP3 into image...').start();
const result = await img2mp3.encode(image, mp3, output);
spinner.succeed(chalk.green('Encoding completed successfully!'));
console.log(chalk.cyan('\nResults:'));
console.log(`📁 Output file: ${chalk.yellow(output)}`);
console.log(`📊 Original image: ${chalk.blue(formatBytes(result.originalImageSize))}`);
console.log(`🎵 MP3 file: ${chalk.blue(formatBytes(result.mp3Size))}`);
console.log(`💾 Final .i2m file: ${chalk.blue(formatBytes(result.outputSize))}`);
console.log(`📐 Container image size: ${chalk.blue(result.imageSize)}`);
if (options.verbose) {
console.log(chalk.gray('\nDetailed info:'));
console.log(chalk.gray(JSON.stringify(result, null, 2)));
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
/**
* Decode command
*/
program
.command('decode')
.description('Decode .i2m file back to image and MP3')
.argument('<i2m>', '.i2m file to decode')
.option('-i, --image <path>', 'Output path for extracted image')
.option('-m, --mp3 <path>', 'Output path for extracted MP3')
.option('-v, --verbose', 'Show detailed output')
.action(async (i2mFile, options) => {
try {
if (!fs.existsSync(i2mFile)) {
console.error(chalk.red(`Error: .i2m file '${i2mFile}' not found`));
process.exit(1);
}
// Generate output filenames if not provided
const baseName = path.basename(i2mFile, '.i2m');
const imagePath = options.image || `${baseName}_extracted_image.png`;
const mp3Path = options.mp3 || `${baseName}_extracted.mp3`;
const spinner = ora('Decoding .i2m file...').start();
const result = await img2mp3.decode(i2mFile, imagePath, mp3Path);
spinner.succeed(chalk.green('Decoding completed successfully!'));
console.log(chalk.cyan('\nExtracted files:'));
console.log(`🖼️ Image: ${chalk.yellow(imagePath)} (${chalk.blue(formatBytes(result.extractedImageSize))})`);
console.log(`🎵 MP3: ${chalk.yellow(mp3Path)} (${chalk.blue(formatBytes(result.extractedMp3Size))})`);
if (options.verbose) {
console.log(chalk.gray('\nDetailed info:'));
console.log(chalk.gray(JSON.stringify(result, null, 2)));
}
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
/**
* Play command
*/
program
.command('play')
.description('Play audio from .i2m file')
.argument('<i2m>', '.i2m file to play')
.option('-v, --verbose', 'Show detailed output')
.action(async (i2mFile, options) => {
try {
if (!fs.existsSync(i2mFile)) {
console.error(chalk.red(`Error: .i2m file '${i2mFile}' not found`));
process.exit(1);
}
console.log(chalk.cyan(`🎵 Playing: ${path.basename(i2mFile)}`));
if (options.verbose) {
const info = await img2mp3.getInfo(i2mFile);
if (info.isValid) {
console.log(chalk.gray(`Audio size: ${formatBytes(info.mp3Size)}`));
console.log(chalk.gray(`Container size: ${formatBytes(info.containerSize)}`));
}
}
await img2mp3.play(i2mFile);
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
/**
* Info command
*/
program
.command('info')
.description('Show information about .i2m file')
.argument('<i2m>', '.i2m file to analyze')
.action(async (i2mFile) => {
try {
if (!fs.existsSync(i2mFile)) {
console.error(chalk.red(`Error: .i2m file '${i2mFile}' not found`));
process.exit(1);
}
const spinner = ora('Analyzing .i2m file...').start();
const info = await img2mp3.getInfo(i2mFile);
spinner.stop();
if (!info.isValid) {
console.error(chalk.red(`Error: ${info.error}`));
process.exit(1);
}
console.log(chalk.cyan(`\n📊 Information for: ${path.basename(i2mFile)}`));
console.log(chalk.green('✅ Valid .i2m file'));
console.log(`🎵 Embedded audio: ${chalk.yellow(formatBytes(info.mp3Size))}`);
console.log(`🖼️ Original image: ${chalk.yellow(formatBytes(info.originalImageSize))}`);
console.log(`📦 Container file: ${chalk.yellow(formatBytes(info.containerSize))}`);
console.log(`📐 Container dimensions: ${chalk.blue(info.dimensions)}`);
console.log(`🏷️ Format: ${chalk.blue(info.format)}`);
} catch (error) {
console.error(chalk.red(`Error: ${error.message}`));
process.exit(1);
}
});
/**
* Interactive mode
*/
program
.command('interactive')
.alias('i')
.description('Interactive mode for easy usage')
.action(async () => {
console.log(chalk.cyan('🎵 Welcome to IMG2MP3 Interactive Mode!\n'));
try {
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: '📥 Encode MP3 into image', value: 'encode' },
{ name: '📤 Decode .i2m file', value: 'decode' },
{ name: '▶️ Play .i2m file', value: 'play' },
{ name: '📊 Show .i2m file info', value: 'info' },
{ name: '❌ Exit', value: 'exit' }
]
}
]);
if (action === 'exit') {
console.log(chalk.yellow('👋 Goodbye!'));
return;
}
switch (action) {
case 'encode':
await interactiveEncode();
break;
case 'decode':
await interactiveDecode();
break;
case 'play':
await interactivePlay();
break;
case 'info':
await interactiveInfo();
break;
}
} catch (error) {
if (error.isTtyError) {
console.error(chalk.red('Interactive mode not supported in this environment'));
} else {
console.error(chalk.red(`Error: ${error.message}`));
}
}
});
/**
* Interactive encode function
*/
async function interactiveEncode() {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'image',
message: 'Path to source image:',
validate: (input) => {
if (!input.trim()) return 'Please enter a path';
if (!fs.existsSync(input)) return 'File not found';
return true;
}
},
{
type: 'input',
name: 'mp3',
message: 'Path to MP3 file:',
validate: (input) => {
if (!input.trim()) return 'Please enter a path';
if (!fs.existsSync(input)) return 'File not found';
return true;
}
},
{
type: 'input',
name: 'output',
message: 'Output .i2m file (optional):',
default: (answers) => {
const baseName = path.basename(answers.mp3, path.extname(answers.mp3));
return `${baseName}.i2m`;
}
}
]);
const spinner = ora('Encoding...').start();
try {
const result = await img2mp3.encode(answers.image, answers.mp3, answers.output);
spinner.succeed(chalk.green('Encoding completed!'));
console.log(chalk.cyan('\n📁 Created:'), chalk.yellow(answers.output));
console.log(chalk.cyan('💾 Size:'), chalk.blue(formatBytes(result.outputSize)));
} catch (error) {
spinner.fail(chalk.red(`Encoding failed: ${error.message}`));
}
}
/**
* Interactive decode function
*/
async function interactiveDecode() {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'i2m',
message: 'Path to .i2m file:',
validate: (input) => {
if (!input.trim()) return 'Please enter a path';
if (!fs.existsSync(input)) return 'File not found';
return true;
}
}
]);
const baseName = path.basename(answers.i2m, '.i2m');
const spinner = ora('Decoding...').start();
try {
const result = await img2mp3.decode(
answers.i2m,
`${baseName}_image.png`,
`${baseName}.mp3`
);
spinner.succeed(chalk.green('Decoding completed!'));
console.log(chalk.cyan('\n📁 Extracted files:'));
console.log(`🖼️ ${baseName}_image.png`);
console.log(`🎵 ${baseName}.mp3`);
} catch (error) {
spinner.fail(chalk.red(`Decoding failed: ${error.message}`));
}
}
/**
* Interactive play function
*/
async function interactivePlay() {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'i2m',
message: 'Path to .i2m file to play:',
validate: (input) => {
if (!input.trim()) return 'Please enter a path';
if (!fs.existsSync(input)) return 'File not found';
return true;
}
}
]);
try {
console.log(chalk.cyan(`\n🎵 Playing: ${path.basename(answers.i2m)}`));
await img2mp3.play(answers.i2m);
} catch (error) {
console.error(chalk.red(`Playback failed: ${error.message}`));
}
}
/**
* Interactive info function
*/
async function interactiveInfo() {
const answers = await inquirer.prompt([
{
type: 'input',
name: 'i2m',
message: 'Path to .i2m file to analyze:',
validate: (input) => {
if (!input.trim()) return 'Please enter a path';
if (!fs.existsSync(input)) return 'File not found';
return true;
}
}
]);
const spinner = ora('Analyzing...').start();
try {
const info = await img2mp3.getInfo(answers.i2m);
spinner.stop();
if (!info.isValid) {
console.error(chalk.red(`Error: ${info.error}`));
return;
}
console.log(chalk.cyan(`\n📊 File: ${path.basename(answers.i2m)}`));
console.log(chalk.green('✅ Valid .i2m file'));
console.log(`🎵 Audio: ${formatBytes(info.mp3Size)}`);
console.log(`🖼️ Image: ${formatBytes(info.originalImageSize)}`);
console.log(`📦 Total: ${formatBytes(info.containerSize)}`);
} catch (error) {
spinner.fail(chalk.red(`Analysis failed: ${error.message}`));
}
}
/**
* Format bytes to human readable format
*/
function formatBytes(bytes, decimals = 2) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
// Handle cleanup on exit
process.on('SIGINT', () => {
console.log(chalk.yellow('\n\n👋 Cleaning up and exiting...'));
img2mp3.cleanup();
process.exit(0);
});
process.on('SIGTERM', () => {
img2mp3.cleanup();
process.exit(0);
});
// Parse arguments
program.parse();

62
examples/basic_usage.js Archivo normal
Ver fichero

@@ -0,0 +1,62 @@
const { IMG2MP3 } = require('../src/index');
const path = require('path');
async function example() {
const img2mp3 = new IMG2MP3();
console.log('🎵 IMG2MP3 Example Usage\n');
try {
// Note: You'll need to provide actual image and MP3 files
const imagePath = 'sample_image.jpg'; // Replace with actual image
const mp3Path = 'sample_audio.mp3'; // Replace with actual MP3
const outputPath = 'example_output.i2m';
console.log('1. Encoding MP3 into image...');
// Uncomment when you have actual files:
// const encodeResult = await img2mp3.encode(imagePath, mp3Path, outputPath);
// console.log('✅ Encoding successful!');
// console.log(` Output size: ${encodeResult.outputSize} bytes`);
// console.log(` Container image: ${encodeResult.imageSize}\n`);
console.log('2. Getting file information...');
// Uncomment when you have an actual .i2m file:
// const info = await img2mp3.getInfo(outputPath);
// if (info.isValid) {
// console.log('✅ Valid .i2m file detected!');
// console.log(` Embedded audio: ${info.mp3Size} bytes`);
// console.log(` Original image: ${info.originalImageSize} bytes\n`);
// }
console.log('3. Playing embedded audio...');
// Uncomment when you have an actual .i2m file:
// await img2mp3.play(outputPath);
// console.log('✅ Playback finished!\n');
console.log('4. Decoding back to separate files...');
// Uncomment when you have an actual .i2m file:
// const decodeResult = await img2mp3.decode(
// outputPath,
// 'extracted_image.png',
// 'extracted_audio.mp3'
// );
// console.log('✅ Decoding successful!');
// console.log(` Extracted image: ${decodeResult.extractedImageSize} bytes`);
// console.log(` Extracted audio: ${decodeResult.extractedMp3Size} bytes`);
console.log('\n🎉 Example completed!');
console.log('\nTo run this example with real files:');
console.log('1. Place an image file named "sample_image.jpg" in this directory');
console.log('2. Place an MP3 file named "sample_audio.mp3" in this directory');
console.log('3. Uncomment the code above and run again');
} catch (error) {
console.error('❌ Error:', error.message);
} finally {
// Clean up any temporary files
img2mp3.cleanup();
}
}
// Run the example
example().catch(console.error);

76
index.d.ts vendido Archivo normal
Ver fichero

@@ -0,0 +1,76 @@
// Type definitions for img2mp3
export interface EncodeResult {
success: boolean;
originalImageSize: number;
mp3Size: number;
outputSize: number;
imageSize: string;
}
export interface DecodeResult {
success: boolean;
extractedImageSize: number;
extractedMp3Size: number;
originalImagePath: string;
mp3Path: string;
}
export interface FileInfo {
isValid: boolean;
mp3Size?: number;
originalImageSize?: number;
imageWidth?: number;
containerSize?: number;
format?: string;
dimensions?: string;
error?: string;
}
export interface PlaybackStatus {
isPlaying: boolean;
hasActiveProcess: boolean;
}
export interface PlayOptions {
[key: string]: any;
}
export declare class MP3ImageEncoder {
constructor();
encode(imagePath: string, mp3Path: string, outputPath: string): Promise<EncodeResult>;
decode(i2mPath: string, outputImagePath: string, outputMp3Path: string): Promise<DecodeResult>;
getInfo(i2mPath: string): Promise<FileInfo>;
}
export declare class I2MPlayer {
constructor();
play(i2mPath: string, options?: PlayOptions): Promise<void>;
stop(): void;
getStatus(): PlaybackStatus;
cleanup(): void;
cleanupAll(): void;
}
export declare class IMG2MP3 {
constructor();
encode(imagePath: string, mp3Path: string, outputPath: string): Promise<EncodeResult>;
decode(i2mPath: string, outputImagePath: string, outputMp3Path: string): Promise<DecodeResult>;
getInfo(i2mPath: string): Promise<FileInfo>;
play(i2mPath: string, options?: PlayOptions): Promise<void>;
stop(): void;
getPlaybackStatus(): PlaybackStatus;
cleanup(): void;
}
// Convenience functions
export declare function encode(imagePath: string, mp3Path: string, outputPath: string): Promise<EncodeResult>;
export declare function decode(i2mPath: string, outputImagePath: string, outputMp3Path: string): Promise<DecodeResult>;
export declare function play(i2mPath: string, options?: PlayOptions): Promise<void>;
export declare function getInfo(i2mPath: string): Promise<FileInfo>;
export {
IMG2MP3 as default,
MP3ImageEncoder,
I2MPlayer
};

58
package.json Archivo normal
Ver fichero

@@ -0,0 +1,58 @@
{
"name": "img2mp3",
"version": "1.0.0",
"description": "Encode MP3 files into images and decode them back, with a built-in player for audio-embedded images",
"main": "src/index.js",
"types": "index.d.ts",
"bin": {
"img2mp3": "./bin/cli.js"
},
"scripts": {
"start": "node src/index.js",
"test": "echo \"Error: no test specified\" && exit 1",
"prepublishOnly": "npm run test",
"example": "node examples/basic_usage.js"
},
"keywords": [
"steganography",
"mp3",
"image",
"audio",
"encoder",
"decoder",
"player",
"cli",
"embed",
"hide",
"extract",
"i2m"
],
"author": "ale",
"license": "MIT",
"dependencies": {
"commander": "^11.1.0",
"sharp": "^0.32.6",
"chalk": "^4.1.2",
"ora": "^5.4.1",
"inquirer": "^8.2.6"
},
"engines": {
"node": ">=14.0.0"
},
"repository": {
"type": "git",
"url": "https://github.com/ale/img2mp3.git"
},
"bugs": {
"url": "https://github.com/ale/img2mp3/issues"
},
"homepage": "https://github.com/ale/img2mp3#readme",
"files": [
"src/",
"bin/",
"examples/",
"index.d.ts",
"README.md",
"LICENSE"
]
}

184
src/encoder.js Archivo normal
Ver fichero

@@ -0,0 +1,184 @@
const fs = require('fs');
const sharp = require('sharp');
/**
* Encoder class for embedding MP3 data into images
*/
class MP3ImageEncoder {
constructor() {
this.magic = Buffer.from('I2M3', 'ascii'); // Magic bytes to identify our format
}
/**
* Encode MP3 file data into an image
* @param {string} imagePath - Path to source image
* @param {string} mp3Path - Path to MP3 file
* @param {string} outputPath - Path for output .i2m file
* @returns {Promise<void>}
*/
async encode(imagePath, mp3Path, outputPath) {
try {
// Read the MP3 file
const mp3Data = fs.readFileSync(mp3Path);
const mp3Size = mp3Data.length;
// Read and process the image
const imageBuffer = fs.readFileSync(imagePath);
const image = sharp(imageBuffer);
const metadata = await image.metadata();
// Convert image to PNG to ensure we can modify pixels reliably
let processedImage = await image.png().toBuffer();
// Create header with magic bytes, MP3 size, and original image size
const header = Buffer.alloc(16);
this.magic.copy(header, 0); // Magic bytes (4 bytes)
header.writeUInt32BE(mp3Size, 4); // MP3 size (4 bytes)
header.writeUInt32BE(imageBuffer.length, 8); // Original image size (4 bytes)
header.writeUInt32BE(metadata.width, 12); // Image width (4 bytes)
// Combine header + original image + MP3 data
const combinedData = Buffer.concat([header, imageBuffer, mp3Data]);
// Calculate required image size to hold all data
const requiredPixels = Math.ceil(combinedData.length / 3); // 3 bytes per pixel (RGB)
const imageSize = Math.ceil(Math.sqrt(requiredPixels));
// Convert combined data to pixel data
const pixelData = Buffer.alloc(imageSize * imageSize * 3);
combinedData.copy(pixelData, 0);
// Fill remaining space with random-looking data
for (let i = combinedData.length; i < pixelData.length; i++) {
pixelData[i] = Math.floor(Math.random() * 256);
}
// Save as PNG and then rename to .i2m
const tempPath = outputPath.replace(/\.i2m$/, '.temp.png');
await sharp(pixelData, {
raw: {
width: imageSize,
height: imageSize,
channels: 3
}
}).png().toFile(tempPath);
// Rename to .i2m extension
fs.renameSync(tempPath, outputPath);
return {
success: true,
originalImageSize: imageBuffer.length,
mp3Size: mp3Size,
outputSize: fs.statSync(outputPath).size,
imageSize: `${imageSize}x${imageSize}`
};
} catch (error) {
throw new Error(`Encoding failed: ${error.message}`);
}
}
/**
* Decode MP3 data from an .i2m image file
* @param {string} i2mPath - Path to .i2m file
* @param {string} outputImagePath - Path for extracted image
* @param {string} outputMp3Path - Path for extracted MP3
* @returns {Promise<Object>}
*/
async decode(i2mPath, outputImagePath, outputMp3Path) {
try {
// Read the .i2m file as image
const imageBuffer = fs.readFileSync(i2mPath);
const image = sharp(imageBuffer);
const metadata = await image.metadata();
// Get raw pixel data
const data = await image.raw().toBuffer();
// Extract header (first 16 bytes)
const header = data.slice(0, 16);
// Verify magic bytes
const magic = header.slice(0, 4);
if (!magic.equals(this.magic)) {
throw new Error('Invalid .i2m file: magic bytes not found');
}
// Extract sizes from header
const mp3Size = header.readUInt32BE(4);
const originalImageSize = header.readUInt32BE(8);
const imageWidth = header.readUInt32BE(12);
// Extract original image data
const originalImageStart = 16;
const originalImageEnd = originalImageStart + originalImageSize;
const originalImageData = data.slice(originalImageStart, originalImageEnd);
// Extract MP3 data
const mp3Start = originalImageEnd;
const mp3End = mp3Start + mp3Size;
const mp3Data = data.slice(mp3Start, mp3End);
// Save extracted files
fs.writeFileSync(outputImagePath, originalImageData);
fs.writeFileSync(outputMp3Path, mp3Data);
return {
success: true,
extractedImageSize: originalImageData.length,
extractedMp3Size: mp3Data.length,
originalImagePath: outputImagePath,
mp3Path: outputMp3Path
};
} catch (error) {
throw new Error(`Decoding failed: ${error.message}`);
}
}
/**
* Get information about an .i2m file without extracting
* @param {string} i2mPath - Path to .i2m file
* @returns {Promise<Object>}
*/
async getInfo(i2mPath) {
try {
const imageBuffer = fs.readFileSync(i2mPath);
const image = sharp(imageBuffer);
const metadata = await image.metadata();
// Get raw pixel data (just the header)
const data = await image.raw().toBuffer();
const header = data.slice(0, 16);
// Verify magic bytes
const magic = header.slice(0, 4);
if (!magic.equals(this.magic)) {
throw new Error('Invalid .i2m file: magic bytes not found');
}
const mp3Size = header.readUInt32BE(4);
const originalImageSize = header.readUInt32BE(8);
const imageWidth = header.readUInt32BE(12);
return {
isValid: true,
mp3Size: mp3Size,
originalImageSize: originalImageSize,
imageWidth: imageWidth,
containerSize: fs.statSync(i2mPath).size,
format: metadata.format,
dimensions: `${metadata.width}x${metadata.height}`
};
} catch (error) {
return {
isValid: false,
error: error.message
};
}
}
}
module.exports = MP3ImageEncoder;

99
src/index.js Archivo normal
Ver fichero

@@ -0,0 +1,99 @@
const MP3ImageEncoder = require('./encoder');
const I2MPlayer = require('./player');
/**
* Main IMG2MP3 class that provides the complete API
*/
class IMG2MP3 {
constructor() {
this.encoder = new MP3ImageEncoder();
this.player = new I2MPlayer();
}
/**
* Encode MP3 into image, creating .i2m file
* @param {string} imagePath - Path to source image
* @param {string} mp3Path - Path to MP3 file
* @param {string} outputPath - Path for output .i2m file
* @returns {Promise<Object>}
*/
async encode(imagePath, mp3Path, outputPath) {
return await this.encoder.encode(imagePath, mp3Path, outputPath);
}
/**
* Decode .i2m file back to image and MP3
* @param {string} i2mPath - Path to .i2m file
* @param {string} outputImagePath - Path for extracted image
* @param {string} outputMp3Path - Path for extracted MP3
* @returns {Promise<Object>}
*/
async decode(i2mPath, outputImagePath, outputMp3Path) {
return await this.encoder.decode(i2mPath, outputImagePath, outputMp3Path);
}
/**
* Get information about .i2m file
* @param {string} i2mPath - Path to .i2m file
* @returns {Promise<Object>}
*/
async getInfo(i2mPath) {
return await this.encoder.getInfo(i2mPath);
}
/**
* Play .i2m file
* @param {string} i2mPath - Path to .i2m file
* @param {Object} options - Playback options
* @returns {Promise<void>}
*/
async play(i2mPath, options = {}) {
return await this.player.play(i2mPath, options);
}
/**
* Stop current playback
*/
stop() {
this.player.stop();
}
/**
* Get playback status
* @returns {Object}
*/
getPlaybackStatus() {
return this.player.getStatus();
}
/**
* Clean up temporary files
*/
cleanup() {
this.player.cleanupAll();
}
}
// Export both the main class and individual components
module.exports = {
IMG2MP3,
MP3ImageEncoder,
I2MPlayer,
// Convenience exports
encode: async (imagePath, mp3Path, outputPath) => {
const encoder = new MP3ImageEncoder();
return await encoder.encode(imagePath, mp3Path, outputPath);
},
decode: async (i2mPath, outputImagePath, outputMp3Path) => {
const encoder = new MP3ImageEncoder();
return await encoder.decode(i2mPath, outputImagePath, outputMp3Path);
},
play: async (i2mPath, options = {}) => {
const player = new I2MPlayer();
return await player.play(i2mPath, options);
},
getInfo: async (i2mPath) => {
const encoder = new MP3ImageEncoder();
return await encoder.getInfo(i2mPath);
}
};

234
src/player.js Archivo normal
Ver fichero

@@ -0,0 +1,234 @@
const fs = require('fs');
const { spawn } = require('child_process');
const path = require('path');
const MP3ImageEncoder = require('./encoder');
/**
* Player class for .i2m files
*/
class I2MPlayer {
constructor() {
this.encoder = new MP3ImageEncoder();
this.currentProcess = null;
this.tempDir = path.join(__dirname, '..', 'temp');
this.isPlaying = false;
}
/**
* Ensure temp directory exists
*/
ensureTempDir() {
if (!fs.existsSync(this.tempDir)) {
fs.mkdirSync(this.tempDir, { recursive: true });
}
}
/**
* Play an .i2m file
* @param {string} i2mPath - Path to .i2m file
* @param {Object} options - Playback options
* @returns {Promise<void>}
*/
async play(i2mPath, options = {}) {
try {
this.ensureTempDir();
// Generate temp file names
const timestamp = Date.now();
const tempMp3Path = path.join(this.tempDir, `temp_${timestamp}.mp3`);
const tempImagePath = path.join(this.tempDir, `temp_${timestamp}_image.png`);
console.log('Extracting audio from image...');
// Extract MP3 from .i2m file
const result = await this.encoder.decode(i2mPath, tempImagePath, tempMp3Path);
if (!result.success) {
throw new Error('Failed to extract audio from image');
}
console.log(`Audio extracted successfully (${result.extractedMp3Size} bytes)`);
// Try different audio players based on platform
await this.playMp3File(tempMp3Path, options);
// Cleanup temp files
this.cleanup([tempMp3Path, tempImagePath]);
} catch (error) {
throw new Error(`Playback failed: ${error.message}`);
}
}
/**
* Play MP3 file using available system players
* @param {string} mp3Path - Path to MP3 file
* @param {Object} options - Playback options
*/
async playMp3File(mp3Path, options = {}) {
const players = [
'mpg123',
'mpv',
'ffplay',
'cvlc',
'play' // sox
];
for (const player of players) {
if (await this.isPlayerAvailable(player)) {
return await this.playWithPlayer(player, mp3Path, options);
}
}
// Fallback: try to use Node.js Speaker (requires compilation)
try {
await this.playWithNodeSpeaker(mp3Path);
} catch (error) {
throw new Error(`No audio player found. Please install one of: ${players.join(', ')}`);
}
}
/**
* Check if a player is available
* @param {string} playerName - Name of the player
* @returns {Promise<boolean>}
*/
async isPlayerAvailable(playerName) {
return new Promise((resolve) => {
const process = spawn('which', [playerName], { stdio: 'ignore' });
process.on('close', (code) => {
resolve(code === 0);
});
});
}
/**
* Play with specific player
* @param {string} player - Player command
* @param {string} mp3Path - Path to MP3 file
* @param {Object} options - Playback options
*/
async playWithPlayer(player, mp3Path, options = {}) {
return new Promise((resolve, reject) => {
let args = [];
switch (player) {
case 'mpg123':
args = ['-q', mp3Path]; // -q for quiet mode
break;
case 'mpv':
args = ['--no-video', '--really-quiet', mp3Path];
break;
case 'ffplay':
args = ['-nodisp', '-autoexit', '-loglevel', 'quiet', mp3Path];
break;
case 'cvlc':
args = ['--intf', 'dummy', '--play-and-exit', mp3Path];
break;
case 'play':
args = [mp3Path];
break;
default:
args = [mp3Path];
}
console.log(`Playing with ${player}...`);
console.log('Press Ctrl+C to stop playback');
this.currentProcess = spawn(player, args, {
stdio: ['inherit', 'pipe', 'pipe']
});
this.isPlaying = true;
this.currentProcess.on('close', (code) => {
this.isPlaying = false;
this.currentProcess = null;
if (code === 0) {
console.log('\nPlayback finished');
resolve();
} else if (code !== null) {
reject(new Error(`Player exited with code ${code}`));
}
});
this.currentProcess.on('error', (error) => {
this.isPlaying = false;
this.currentProcess = null;
reject(new Error(`Player error: ${error.message}`));
});
// Handle Ctrl+C
process.on('SIGINT', () => {
this.stop();
});
});
}
/**
* Play using Node.js Speaker (fallback)
* @param {string} mp3Path - Path to MP3 file
*/
async playWithNodeSpeaker(mp3Path) {
// Node.js Speaker requires native compilation and is not reliably available
// We recommend installing a system audio player instead
throw new Error('No system audio player found. Please install: mpg123, mpv, ffplay, vlc, or sox');
}
/**
* Stop current playback
*/
stop() {
if (this.currentProcess && this.isPlaying) {
this.currentProcess.kill('SIGTERM');
this.isPlaying = false;
console.log('\nPlayback stopped');
}
}
/**
* Get playback status
* @returns {Object}
*/
getStatus() {
return {
isPlaying: this.isPlaying,
hasActiveProcess: !!this.currentProcess
};
}
/**
* Clean up temporary files
* @param {Array<string>} files - Array of file paths to delete
*/
cleanup(files) {
files.forEach(file => {
try {
if (fs.existsSync(file)) {
fs.unlinkSync(file);
}
} catch (error) {
console.warn(`Warning: Could not delete temp file ${file}`);
}
});
}
/**
* Clean up all temp files
*/
cleanupAll() {
try {
if (fs.existsSync(this.tempDir)) {
const files = fs.readdirSync(this.tempDir);
files.forEach(file => {
const filePath = path.join(this.tempDir, file);
fs.unlinkSync(filePath);
});
}
} catch (error) {
console.warn('Warning: Could not clean up temp directory');
}
}
}
module.exports = I2MPlayer;