npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@eplayer/emp4-core

v0.0.3

Published

EMP4 视频转换核心库 - 提供加密、编码、FFmpeg封装等核心功能

Readme

@eplayer/emp4-core

EMP4 视频转换核心库 — 提供加密、编码、FFmpeg 封装等核心功能。

目录

简介

@eplayer/emp4-core 是 EMP4 转换器的底层核心库,负责视频文件的加密编码、格式转换和流媒体清单生成。EMP4 是一种基于 AES-256-GCM 加密的视频容器格式,支持分片播放和多码率自适应流。

该库提供:

  • 底层 API — 直接操作加密、编码、转码等核心功能
  • 类型安全 — 完整的 TypeScript 类型定义
  • 模块化设计 — 各模块职责明确,可独立使用
  • 流式处理 — 支持大文件分块加密,内存占用可控

功能特性

| 特性 | 说明 | |------|------| | AES-256-GCM 加密 | 使用 PBKDF2 密钥派生 + GCM 认证加密,提供机密性和完整性保护 | | FFmpeg 封装 | 统一的转码、探针、缩略图提取接口,支持进度回调 | | 单文件模式 | 将整个视频编码为一个 EMP4 文件,适合离线播放 | | 分片模式 | 按时长分割为多个 EMP4 分片,适合流媒体传输 | | EPL 清单生成 | 类似 HLS m3u8 的加密流媒体清单(JSON 格式) | | 多码率自适应 | 支持同时生成多个分辨率/码率版本 | | PIN 保护 | 可选的额外 PIN 码访问控制 | | 完整 TypeScript 类型 | 全量类型定义导出,IDE 友好 |

EMP4 文件格式

整体结构

┌──────────────────────────────────────┐
│           文件头部 (128 字节)          │  魔数 + 版本 + 盐值 + IV + 校验和 + 元数据长度
├──────────────────────────────────────┤
│           元数据 (JSON)               │  视频信息:分辨率、码率、编解码器等
├──────────────────────────────────────┤
│           加密数据块 1                │  4字节长度 + 密文 + 16字节 GCM 标签
├──────────────────────────────────────┤
│           加密数据块 2                │
│              ...                     │
├──────────────────────────────────────┤
│           文件尾部 (12 字节)          │  4字节魔数 + 8字节总大小
└──────────────────────────────────────┘

文件头部结构(128 字节)

| 偏移量 | 大小 | 描述 | |--------|------|------| | 0 | 4 | 魔数 "EMP4" | | 4 | 8 | 版本号(如 "1.0.0") | | 12 | 4 | 头部大小(128) | | 16 | 1 | 加密算法标识(0x00 = AES-256-GCM) | | 17 | 1 | 密钥派生算法(0x00 = PBKDF2-SHA256) | | 18 | 32 | 盐值(Salt) | | 50 | 12 | 初始化向量(IV) | | 62 | 4 | 原始格式(如 "mp4") | | 66 | 8 | 原始文件大小 | | 74 | 32 | 校验和(SHA-256) | | 106 | 4 | 元数据长度 | | 110 | 18 | 保留字段 |

加密数据块结构

每个加密数据块包含:

┌──────────────┬──────────────────┬──────────────────┐
│ 4 字节长度   │    加密数据      │   16 字节标签    │
│ (Little Endian)│   (密文)       │   (GCM Auth Tag) │
└──────────────┴──────────────────┴──────────────────┘

文件尾部结构(12 字节)

| 偏移量 | 大小 | 描述 | |--------|------|------| | 0 | 4 | 魔数 "EMP4" | | 4 | 8 | 文件总大小(Big Endian) |

安装

npm install @eplayer/emp4-core

环境要求

  • Node.js: >= 18.0.0
  • FFmpeg: 需要系统安装 FFmpeg 并确保 ffmpegffprobe 命令可用

验证 FFmpeg 安装

ffmpeg -version
ffprobe -version

快速开始

基本用法

import { EMP4Encoder, FFmpegWrapper, mergeConfig } from '@eplayer/emp4-core'

// 1. 配置
const config = mergeConfig({
  ffmpegPath: 'ffmpeg',
  ffprobePath: 'ffprobe',
  defaultPassword: 'your-secret-password'
})

// 2. 获取视频信息
const ffmpeg = new FFmpegWrapper(config.ffmpegPath, config.ffprobePath)
const videoInfo = await ffmpeg.getVideoInfo('input.mp4')

// 3. 创建编码器并编码
const encoder = new EMP4Encoder(config.defaultPassword)
await encoder.encode('input.mp4', 'output.emp4', videoInfo)

使用 Converter 命名空间

import Converter from '@eplayer/emp4-core'

// 通过统一入口访问所有功能
const config = Converter.mergeConfig({ ffmpegPath: 'ffmpeg' })
const ffmpeg = new Converter.FFmpegWrapper(config.ffmpegPath, config.ffprobePath)
const encoder = new Converter.EMP4Encoder(config.defaultPassword)

分片模式(流媒体)

import { EMP4Encoder, FFmpegWrapper, EPLGenerator, generateKeyId } from '@eplayer/emp4-core'

const ffmpeg = new FFmpegWrapper()
const encoder = new EMP4Encoder('your-password')
const eplGenerator = new EPLGenerator(ffmpeg)

// 1. 获取视频信息
const videoInfo = await ffmpeg.getVideoInfo('input.mp4')

// 2. 将视频分割为 MP4 分片
const segments = await ffmpeg.segmentMp4('input.mp4', './output/segments', {
  videoCodec: 'libx264',
  audioCodec: 'aac',
  segmentDuration: 10,
  fragmented: true
})

// 3. 加密每个分片
const segmentResults = await encoder.encodeSegments(
  segments,
  './output/encrypted',
  videoInfo,
  { password: 'your-password' }
)

// 4. 生成 EPL 清单
const manifest = await eplGenerator.generate(videoInfo, [
  { filePath: './output/encrypted/segment-0.emp4', resolution: '1080p', width: 1920, height: 1080, bitrate: 8000000 }
], {
  keyId: generateKeyId(),
  outputDir: './output'
})

await eplGenerator.writeManifest(manifest, './output/manifest.epl')

带进度回调

await encoder.encode('input.mp4', 'output.emp4', videoInfo, {
  onProgress: (info) => {
    console.log(`进度: ${info.percentage}%`)
    console.log(`阶段: ${info.stage}`)
  }
})

模块详解

配置管理 (config)

配置管理模块提供默认配置、配置合并和验证功能。

默认配置

import { DEFAULT_CONFIG } from '@eplayer/emp4-core'

console.log(DEFAULT_CONFIG)
// {
//   ffmpegPath: 'ffmpeg',
//   ffprobePath: 'ffprobe',
//   defaultPassword: 'default-encryption-key',
//   defaultChunkSize: 1048576,        // 1MB
//   defaultSegmentDuration: 10,       // 10秒
//   defaultPreset: 'medium',
//   defaultConcurrency: 4
// }

合并配置

import { mergeConfig } from '@eplayer/emp4-core'

const config = mergeConfig({
  ffmpegPath: '/usr/local/bin/ffmpeg',
  defaultPassword: 'my-secret-key',
  defaultChunkSize: 2 * 1024 * 1024  // 2MB
})

验证配置

import { validateConfig } from '@eplayer/emp4-core'

const result = validateConfig(config)
if (!result.valid) {
  console.error('配置错误:', result.errors)
}

配置选项说明

| 选项 | 类型 | 默认值 | 说明 | |------|------|--------|------| | ffmpegPath | string | 'ffmpeg' | FFmpeg 可执行文件路径 | | ffprobePath | string | 'ffprobe' | FFprobe 可执行文件路径 | | defaultPassword | string | 'default-encryption-key' | 默认加密密码 | | defaultChunkSize | number | 1048576 | 默认分块大小(字节),最小 512KB | | defaultSegmentDuration | number | 10 | 默认分片时长(秒) | | defaultPreset | string | 'medium' | 默认编码预设 | | defaultConcurrency | number | 4 | 默认并发数 |


加密引擎 (crypto)

加密引擎模块提供 AES-256-GCM 加密、解密和文件头尾构建功能。

创建加密引擎

import { CryptoEngine } from '@eplayer/emp4-core'

const crypto = new CryptoEngine('my-password')

密钥派生

// 生成密钥参数(盐值和 IV)
const keyParams = crypto.generateKeyParams()
console.log('盐值长度:', keyParams.salt.length)  // 32 字节
console.log('IV 长度:', keyParams.iv.length)     // 12 字节

// 从密码派生密钥
const key = crypto.deriveKey('my-password', keyParams.salt)
console.log('密钥长度:', key.length)  // 32 字节 (256 位)

加密与解密

const data = Buffer.from('Hello, World!')

// 加密
const encrypted = crypto.encryptChunk(data, key, keyParams.iv)
console.log('密文长度:', encrypted.data.length)
console.log('认证标签长度:', encrypted.authTag.length)  // 16 字节

// 解密
const decrypted = crypto.decryptChunk(
  encrypted.data,
  encrypted.authTag,
  key,
  keyParams.iv
)
console.log('解密结果:', decrypted.toString())  // "Hello, World!"

分块 IV 生成

每个数据块使用不同的 IV 以提高安全性:

const chunk0IV = crypto.generateChunkIV(baseIV, 0)
const chunk1IV = crypto.generateChunkIV(baseIV, 1)

文件头尾构建

import { buildHeader, buildFooter, parseHeader, parseFooter, HEADER_SIZE, FOOTER_SIZE } from '@eplayer/emp4-core'

// 构建头部
const header = buildHeader({
  salt: keyParams.salt,
  iv: keyParams.iv,
  format: 'mp4',
  originalSize: BigInt(1024000),
  checksum: crypto.calculateChecksum(Buffer.from('data')),
  metadataLength: 256
})

// 构建尾部
const footer = buildFooter(BigInt(1024000 + HEADER_SIZE + 256 + FOOTER_SIZE))

// 解析头部
const headerInfo = parseHeader(header)
console.log('魔数:', headerInfo.magic)        // "EMP4"
console.log('版本:', headerInfo.version)      // "1.0.0"
console.log('盐值:', headerInfo.salt)
console.log('IV:', headerInfo.iv)

// 解析尾部
const footerInfo = parseFooter(footer)
console.log('总大小:', footerInfo.totalSize)

常量导出

import { HEADER_SIZE, FOOTER_SIZE, MAGIC, VERSION } from '@eplayer/emp4-core'

console.log('头部大小:', HEADER_SIZE)  // 128
console.log('尾部大小:', FOOTER_SIZE)  // 12
console.log('魔数:', MAGIC)            // "EMP4"
console.log('版本:', VERSION)          // "1.0.0"

PIN 码保护功能

PIN 码保护为视频文件提供额外的访问控制层。用户必须输入正确的 PIN 码才能播放视频。

职责分离说明

PIN 码功能分为两个端:

| 端 | 库 | 职责 | 方法 | |---|---|---|---| | 加密端 | @eplayer/emp4-core (Node.js) | 生成 PIN 保护信息 | generatePinSalt(), hashPin(), generatePinProtection() | | 解密端 | @eplayer/epl.js (浏览器) | 验证 PIN 并解密 | verifyPin(), deriveKeyFromPin() |

converter/core (加密端)              epl (解密端)
┌─────────────────────┐            ┌─────────────────────┐
│ CryptoEngine        │            │ PinVerifier         │
│ ─────────────────── │            │ ─────────────────── │
│ generatePinSalt()   │            │ verifyPin()         │
│ hashPin()           │────────────│ hashPin()           │
│ generatePinProtection()          │ deriveKeyFromPin()  │
└─────────────────────┘            └─────────────────────┘
        │                                    │
        │ 写入元数据                          │ 读取元数据
        ▼                                    ▼
   EMP4 文件                            播放器验证 PIN
   { hasPin: true,                      并派生解密密钥
     pinSalt: "...",
     pinHash: "..." }

生成 PIN 保护信息

import { CryptoEngine } from '@eplayer/emp4-core'

const crypto = new CryptoEngine()

// 为 PIN 码生成保护信息
const pinProtection = crypto.generatePinProtection('1234')

console.log('PIN 盐值 (Base64):', pinProtection.pinSalt)
console.log('PIN 哈希 (Base64):', pinProtection.pinHash)

// 返回值结构
// {
//   salt: Buffer,        // 16 字节的 PIN 盐值
//   hash: Buffer,        // 32 字节的 PIN 哈希值
//   pinSalt: string,     // Base64 编码的盐值,用于存储到元数据
//   pinHash: string      // Base64 编码的哈希值,用于存储到元数据
// }

单独生成 PIN 盐值和哈希

// 生成 PIN 盐值(16 字节)
const pinSalt = crypto.generatePinSalt()

// 计算 PIN 哈希值
const pinHash = crypto.hashPin('1234', pinSalt)

// 转换为 Base64 存储
const pinSaltBase64 = pinSalt.toString('base64')
const pinHashBase64 = pinHash.toString('base64')

PIN 码算法参数

| 参数 | 值 | 说明 | |------|-----|------| | 算法 | PBKDF2-HMAC-SHA256 | 与浏览器端 EPL 库保持一致 | | 迭代次数 | 100,000 | 增加暴力破解难度 | | 密钥长度 | 32 字节 (256 位) | PBKDF2 输出长度 | | PIN 盐值长度 | 16 字节 | 区别于加密盐值(32 字节) |

完整示例:带 PIN 保护的编码

import { CryptoEngine, EMP4Encoder, FFmpegWrapper } from '@eplayer/emp4-core'

const ffmpeg = new FFmpegWrapper()
const encoder = new EMP4Encoder('your-password')
const crypto = new CryptoEngine()

// 获取视频信息
const videoInfo = await ffmpeg.getVideoInfo('input.mp4')

// 生成 PIN 保护信息
const pinProtection = crypto.generatePinProtection('1234')

// 编码时启用 PIN 保护
await encoder.encode('input.mp4', 'output.emp4', videoInfo, {
  title: '受保护的视频',
  password: 'your-password',
  hasPin: true,
  pinSalt: pinProtection.pinSalt,
  pinHash: pinProtection.pinHash
})

FFmpeg 封装 (ffmpeg)

FFmpeg 封装模块提供视频信息获取、转码、分片和缩略图提取功能。

创建 FFmpeg 包装器

import { FFmpegWrapper } from '@eplayer/emp4-core'

const ffmpeg = new FFmpegWrapper('ffmpeg', 'ffprobe')

检查工具可用性

const available = await ffmpeg.checkAvailable()
if (!available) {
  throw new Error('FFmpeg 未安装或不可用')
}

获取视频信息

const videoInfo = await ffmpeg.getVideoInfo('video.mp4')

console.log('时长:', videoInfo.duration, '秒')
console.log('大小:', videoInfo.size, '字节')
console.log('码率:', videoInfo.bitrate, 'bps')
console.log('容器:', videoInfo.container)

// 视频流信息
const videoStream = videoInfo.videoStreams[0]
console.log('视频编解码器:', videoStream.codec)
console.log('分辨率:', videoStream.width, 'x', videoStream.height)
console.log('帧率:', videoStream.fps, 'fps')

// 音频流信息
const audioStream = videoInfo.audioStreams[0]
console.log('音频编解码器:', audioStream.codec)
console.log('采样率:', audioStream.sampleRate, 'Hz')
console.log('声道数:', audioStream.channels)

判断是否需要转码

const decision = ffmpeg.needsTranscode(videoInfo)

if (decision.required) {
  console.log('需要转码,原因:', decision.reasons)
  console.log('推荐配置:', decision.targetConfig)
}

兼容性要求:

  • 视频编解码器:H.264、H.265/HEVC、AVC
  • 音频编解码器:AAC、MP3
  • 容器格式:MP4、MOV、QuickTime

视频转码

await ffmpeg.transcode('input.mp4', 'output.mp4', {
  videoCodec: 'libx264',
  audioCodec: 'aac',
  preset: 'medium',
  crf: 23,
  audioBitrate: '128k',
  resolution: '1920x1080',
  fps: 30,
  fragmented: true
}, (progress) => {
  console.log(`转码进度: ${progress}%`)
})

转码配置选项

| 选项 | 类型 | 说明 | |------|------|------| | videoCodec | 'libx264' \| 'libx265' \| 'copy' | 视频编解码器 | | audioCodec | 'aac' \| 'mp3' \| 'copy' | 音频编解码器 | | preset | string | 编码预设:ultrafast ~ veryslow | | crf | number | 恒定质量因子(0-51,默认 23) | | audioBitrate | string | 音频码率(如 "128k") | | resolution | string | 输出分辨率(如 "1920x1080") | | fps | number | 输出帧率 | | fragmented | boolean | 是否生成分片 MP4 | | chunkSize | number | 分块大小(字节) | | segmentDuration | number | 分片时长(秒) |

视频分片

const segments = await ffmpeg.segmentMp4('input.mp4', './segments', {
  videoCodec: 'libx264',
  audioCodec: 'aac',
  segmentDuration: 10,
  fragmented: true
}, (progress) => {
  console.log(`分片进度: ${progress}%`)
})

// segments 是 SegmentSource 数组
for (const seg of segments) {
  console.log(`分片 ${seg.index}: ${seg.filePath}, ${seg.duration}秒, ${seg.size}字节`)
}

提取缩略图

// 提取到文件
await ffmpeg.getThumbnail('video.mp4', 10, 'thumbnail.jpg')

// 提取为 Buffer
const thumbBuffer = await ffmpeg.getThumbnail('video.mp4', 10)

默认码率配置

import { DEFAULT_BITRATE_PROFILES } from '@eplayer/emp4-core'

// [
//   { name: '1080p', width: 1920, height: 1080, videoBitrate: 8000000, audioBitrate: 192000 },
//   { name: '720p', width: 1280, height: 720, videoBitrate: 5000000, audioBitrate: 128000 },
//   { name: '480p', width: 854, height: 480, videoBitrate: 2500000, audioBitrate: 128000 }
// ]

EMP4 编码器 (encoder)

EMP4 编码器模块提供视频文件到 EMP4 格式的编码功能。

创建编码器

import { EMP4Encoder } from '@eplayer/emp4-core'

const encoder = new EMP4Encoder('my-password', 1024 * 1024)  // 密码, 分块大小

单文件编码

await encoder.encode('input.mp4', 'output.emp4', videoInfo, {
  title: '我的视频',
  password: 'optional-override-password',
  chunkSize: 2 * 1024 * 1024,  // 2MB
  hasPin: false,
  onProgress: (info) => {
    console.log(`${info.percentage}% - ${info.stage}`)
  }
})

编码选项

| 选项 | 类型 | 说明 | |------|------|------| | title | string | 视频标题 | | password | string | 覆盖默认密码 | | hasPin | boolean | 是否启用 PIN 保护 | | pinSalt | string | PIN 盐值 | | pinHash | string | PIN 哈希值 | | chunkSize | number | 分块大小(字节) | | onProgress | (info: ProgressInfo) => void | 进度回调 |

分片编码

const segmentResults = await encoder.encodeSegments(
  segments,           // SegmentSource[]
  './output',         // 输出目录
  videoInfo,          // DetailedVideoInfo
  { password: 'my-password' }
)

// segmentResults 是 SegmentResult[]
for (const result of segmentResults) {
  console.log(`分片 ${result.index}: ${result.filePath}, ${result.size}字节`)
  console.log(`校验和: ${result.checksum}`)
}

读取元数据

const metadata = await encoder.readMetadata('video.emp4')
console.log('标题:', metadata.title)
console.log('时长:', metadata.duration)
console.log('分辨率:', metadata.resolution)
console.log('创建时间:', metadata.createdAt)

工具函数

import { isEMP4File, getDefaultOutputPath, getVideoOutputDir } from '@eplayer/emp4-core'

// 检查文件是否为 EMP4 格式
const isEMP4 = await isEMP4File('video.emp4')

// 获取默认输出路径
const outputPath = getDefaultOutputPath('video.mp4')
// 返回: 'video/manifest.epl'

// 获取视频输出目录
const outputDir = getVideoOutputDir('video.mp4', './output')
// 返回: './output/video'

EPL 清单生成器 (manifest)

EPL 清单生成器模块负责生成 EPL(Encrypted Playlist)流媒体清单文件。

创建生成器

import { EPLGenerator, FFmpegWrapper } from '@eplayer/emp4-core'

const ffmpeg = new FFmpegWrapper()
const eplGenerator = new EPLGenerator(ffmpeg)

生成清单

import { generateKeyId } from '@eplayer/emp4-core'

const manifest = await eplGenerator.generate(videoInfo, outputs, {
  title: '我的视频',
  keyId: generateKeyId(),
  keyServer: 'https://key-server.example.com',
  thumbnailInterval: 30,
  outputDir: './output',
  chunkSize: 1024 * 1024,
  segmentDuration: 10
})

EPL 清单结构

interface EPLManifest {
  version: string                    // "1.0"
  type: 'vod' | 'live'              // 流类型
  duration: number                   // 总时长(秒)
  title: string                      // 标题
  encryption: {
    method: 'AES-256-GCM'
    keyId: string
    keyServer?: string
  }
  chunkConfig: {
    defaultSize: number
    defaultDuration: number
    minSize?: number
    maxSize?: number
  }
  streams: StreamInfo[]              // 多码率流列表
  thumbnails: ThumbnailInfo[]        // 缩略图列表
  chapters?: ChapterInfo[]           // 章节列表(可选)
}

写入清单文件

await eplGenerator.writeManifest(manifest, './output/manifest.epl')

生成缩略图

const thumbnails = await eplGenerator.generateThumbnails(
  'video.mp4',
  './output/thumbnails',
  30,     // 间隔(秒)
  3600    // 总时长(秒)
)

// thumbnails: ThumbnailInfo[]
for (const thumb of thumbnails) {
  console.log(`时间: ${thumb.time}s, URI: ${thumb.uri}`)
}

生成密钥 ID

import { generateKeyId } from '@eplayer/emp4-core'

const keyId = generateKeyId()
// 返回 32 字符的十六进制字符串,如 "a1b2c3d4e5f6..."

类型定义

视频信息类型

// 基础视频信息
interface VideoInfo {
  path: string
  name: string
  format: string
  duration: number
  resolution: string
  bitrate: number
  codec: string
  audioCodec: string
  fileSize: number
  width?: number
  height?: number
  fps?: number
}

// 详细视频信息
interface DetailedVideoInfo {
  duration: number
  size: number
  bitrate: number
  container: string
  videoStreams: VideoStream[]
  audioStreams: AudioStream[]
}

// 视频流信息
interface VideoStream {
  codec: string
  width: number
  height: number
  fps: number
  profile?: string
  level?: number
  bitrate?: number
}

// 音频流信息
interface AudioStream {
  codec: string
  sampleRate: number
  channels: number
  bitrate?: number
}

转码配置类型

interface TranscodeConfig {
  videoCodec?: 'libx264' | 'libx265' | 'copy'
  audioCodec?: 'aac' | 'mp3' | 'copy'
  preset?: 'ultrafast' | 'superfast' | 'veryfast' | 'faster' | 'fast' | 'medium' | 'slow' | 'slower' | 'veryslow'
  crf?: number
  audioBitrate?: string
  resolution?: string
  fps?: number
  fragmented?: boolean
  chunkSize?: number
  segmentDuration?: number
}

interface TranscodeDecision {
  required: boolean
  reasons: string[]
  targetConfig: TranscodeConfig | null
}

加密相关类型

interface KeyParams {
  salt: Buffer
  iv: Buffer
}

interface EncryptedChunk {
  data: Buffer
  authTag: Buffer
}

interface HeaderParams {
  salt: Buffer
  iv: Buffer
  format: string
  originalSize: bigint
  checksum: Buffer
  metadataLength: number
}

EMP4 相关类型

interface EMP4Metadata {
  title?: string
  duration: number
  resolution: string
  bitrate: number
  codec: string
  audioCodec: string
  createdAt: string
  fileSize: number
  fragmented: boolean
  hasPin?: boolean
  pinSalt?: string
  pinHash?: string
}

interface EMP4Output {
  filePath: string
  resolution: string
  width: number
  height: number
  bitrate: number
}

interface SegmentResult {
  index: number
  filePath: string
  size: number
  checksum: string
}

interface SegmentSource {
  index: number
  filePath: string
  duration: number
  size: number
}

EPL 清单类型

interface EPLManifest {
  version: string
  type: 'vod' | 'live'
  duration: number
  title: string
  encryption: EncryptionInfo
  chunkConfig: ChunkConfig
  streams: StreamInfo[]
  thumbnails: ThumbnailInfo[]
  chapters?: ChapterInfo[]
}

interface StreamInfo {
  id: string
  name: string
  bandwidth: number
  resolution: string
  codecs: string
  frameRate: number
  segments: SegmentInfo[]
}

interface SegmentInfo {
  index: number
  duration: number
  size: number
  uri: string
  checksum: string
}

interface ThumbnailInfo {
  time: string
  uri: string
}

interface ChapterInfo {
  title: string
  startTime: number
  endTime: number
}

进度信息类型

interface ProgressInfo {
  percentage: number
  stage: 'analyzing' | 'transcoding' | 'encrypting' | 'generating' | 'completed'
  currentProfile?: string
  eta?: number
  speed?: string
}

完整示例

完整转换流程

import {
  FFmpegWrapper,
  EMP4Encoder,
  EPLGenerator,
  mergeConfig,
  generateKeyId,
  DEFAULT_BITRATE_PROFILES
} from '@eplayer/emp4-core'

async function convertToEMP4(inputPath: string, outputDir: string) {
  // 1. 配置
  const config = mergeConfig({
    ffmpegPath: 'ffmpeg',
    defaultPassword: 'my-secret-password',
    defaultSegmentDuration: 10
  })

  // 2. 初始化组件
  const ffmpeg = new FFmpegWrapper(config.ffmpegPath, config.ffprobePath)
  const encoder = new EMP4Encoder(config.defaultPassword, config.defaultChunkSize)
  const eplGenerator = new EPLGenerator(ffmpeg)

  // 3. 获取视频信息
  const videoInfo = await ffmpeg.getVideoInfo(inputPath)
  console.log(`视频时长: ${videoInfo.duration}秒`)

  // 4. 判断是否需要转码
  const transcodeDecision = ffmpeg.needsTranscode(videoInfo)
  let segments

  if (transcodeDecision.required) {
    console.log('需要转码:', transcodeDecision.reasons)
    
    // 转码并分片
    segments = await ffmpeg.segmentMp4(inputPath, `${outputDir}/temp`, {
      ...transcodeDecision.targetConfig,
      segmentDuration: config.defaultSegmentDuration
    }, (progress) => {
      console.log(`转码进度: ${progress}%`)
    })
  } else {
    // 直接分片
    segments = await ffmpeg.segmentMp4(inputPath, `${outputDir}/temp`, {
      videoCodec: 'copy',
      audioCodec: 'copy',
      segmentDuration: config.defaultSegmentDuration,
      fragmented: true
    })
  }

  // 5. 加密分片
  const encryptedSegments = await encoder.encodeSegments(
    segments,
    `${outputDir}/streams/1080p`,
    videoInfo,
    {
      onProgress: (info) => {
        console.log(`加密进度: ${info.percentage}%`)
      }
    }
  )

  // 6. 生成缩略图
  const thumbnails = await eplGenerator.generateThumbnails(
    inputPath,
    `${outputDir}/thumbnails`,
    30,
    videoInfo.duration
  )

  // 7. 生成 EPL 清单
  const manifest = await eplGenerator.generate(videoInfo, [{
    filePath: encryptedSegments[0].filePath,
    resolution: '1080p',
    width: videoInfo.videoStreams[0].width,
    height: videoInfo.videoStreams[0].height,
    bitrate: videoInfo.bitrate
  }], {
    title: 'My Video',
    keyId: generateKeyId(),
    outputDir,
    thumbnailInterval: 30,
    segmentDuration: config.defaultSegmentDuration
  })

  manifest.thumbnails = thumbnails

  // 8. 写入清单
  await eplGenerator.writeManifest(manifest, `${outputDir}/manifest.epl`)

  console.log('转换完成!')
  return manifest
}

// 使用
convertToEMP4('./input.mp4', './output')

多码率转换

import { FFmpegWrapper, EMP4Encoder, EPLGenerator, generateKeyId, DEFAULT_BITRATE_PROFILES } from '@eplayer/emp4-core'

async function convertMultiBitrate(inputPath: string, outputDir: string) {
  const ffmpeg = new FFmpegWrapper()
  const encoder = new EMP4Encoder('password')
  const eplGenerator = new EPLGenerator(ffmpeg)

  const videoInfo = await ffmpeg.getVideoInfo(inputPath)
  const outputs = []

  for (const profile of DEFAULT_BITRATE_PROFILES) {
    const segmentDir = `${outputDir}/streams/${profile.name}`
    
    // 分片转码
    const segments = await ffmpeg.segmentMp4(inputPath, `${segmentDir}/temp`, {
      videoCodec: 'libx264',
      audioCodec: 'aac',
      resolution: `${profile.width}x${profile.height}`,
      segmentDuration: 10,
      fragmented: true
    })

    // 加密
    const results = await encoder.encodeSegments(segments, segmentDir, videoInfo)

    outputs.push({
      filePath: results[0].filePath,
      resolution: profile.name,
      width: profile.width,
      height: profile.height,
      bitrate: profile.videoBitrate + profile.audioBitrate
    })
  }

  // 生成清单
  const manifest = await eplGenerator.generate(videoInfo, outputs, {
    keyId: generateKeyId(),
    outputDir
  })

  await eplGenerator.writeManifest(manifest, `${outputDir}/manifest.epl`)
}

构建与开发

安装依赖

npm install

构建

# 完整构建(类型声明 + JS 打包)
npm run build

# 仅生成类型声明
npm run build:types

# 仅打包 JS
npm run build:js

清理

npm run clean

构建产物

dist/
├── index.js       # ESM 格式打包产物
├── index.js.map   # Source Map
├── index.d.ts     # 类型声明入口
└── *.d.ts         # 各模块类型声明

许可证

MIT